From bb068f7cec14b93b63b227e284d736ef979892cc Mon Sep 17 00:00:00 2001 From: Monixc Date: Thu, 7 Aug 2025 21:06:57 +0900 Subject: [PATCH 01/30] =?UTF-8?q?fix:=20AuthCallbackPage=20GlobalLayout=20?= =?UTF-8?q?import=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/.gitkeep | 0 src/pages/.gitkeep | 0 src/pages/auth/AuthCallbackPage.tsx | 58 ++++++++++++++--------------- src/types/.gitkeep | 0 4 files changed, 27 insertions(+), 31 deletions(-) delete mode 100644 src/components/common/.gitkeep delete mode 100644 src/pages/.gitkeep delete mode 100644 src/types/.gitkeep diff --git a/src/components/common/.gitkeep b/src/components/common/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/pages/.gitkeep b/src/pages/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/pages/auth/AuthCallbackPage.tsx b/src/pages/auth/AuthCallbackPage.tsx index f7f01cd..bc27c1a 100644 --- a/src/pages/auth/AuthCallbackPage.tsx +++ b/src/pages/auth/AuthCallbackPage.tsx @@ -1,40 +1,36 @@ -import { useNavigate } from 'react-router-dom'; -import GlobalLayout from '@/components/layout/GlobalLayout'; -import { Button } from '@/components/ui/button'; -import { useGitHubCallback } from '@/hooks/useGitHubCallback'; +import { useNavigate } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { useGitHubCallback } from "@/hooks/useGitHubCallback"; export default function AuthCallbackPage() { const navigate = useNavigate(); const { isPending, isError, error } = useGitHubCallback(); return ( - -
- {isPending && ( -
-
-

GitHub 로그인 처리 중...

-

사용자 정보를 확인하고 있습니다

-
- )} +
+ {isPending && ( +
+
+

GitHub 로그인 처리 중...

+

사용자 정보를 확인하고 있습니다

+
+ )} - {isError && ( -
-

로그인 실패

-
-

- {error?.message || '로그인 처리 중 오류가 발생했습니다'} -

-
- + {isError && ( +
+

로그인 실패

+
+

+ {error?.message || "로그인 처리 중 오류가 발생했습니다"} +

- )} -
- + +
+ )} +
); -} \ No newline at end of file +} diff --git a/src/types/.gitkeep b/src/types/.gitkeep deleted file mode 100644 index e69de29..0000000 From 576c6847baec8194ac3c6ce619dac916fe44d4e1 Mon Sep 17 00:00:00 2001 From: Monixc Date: Thu, 7 Aug 2025 21:30:59 +0900 Subject: [PATCH 02/30] =?UTF-8?q?refactor:=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=20=ED=8C=8C=EC=9D=BC=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=83=88=EB=A1=9C=EC=9A=B4=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/PositionSelector.tsx | 2 +- src/components/common/StackSelector.tsx | 2 +- src/hooks/usePost.ts | 2 +- src/mocks/handlers.ts | 10 +++++++--- src/pages/community/__components/CommentItem.tsx | 2 +- src/pages/community/__components/CommentSection.tsx | 2 +- src/pages/community/__components/PostContent.tsx | 2 +- src/pages/community/__components/ReplyItem.tsx | 2 +- .../explore/_components/DeveloperProfileCard.tsx | 2 +- src/pages/explore/_components/layout/GridView.tsx | 2 +- src/pages/explore/_components/layout/ListView.tsx | 2 +- src/pages/explore/index.tsx | 2 +- src/pages/home/_components/ListItem.tsx | 2 +- src/pages/home/index.tsx | 2 +- src/types/{auth.ts => auth.types.ts} | 12 ++---------- src/types/{board.ts => board.types.ts} | 0 src/types/{community.ts => community.types.ts} | 0 src/types/{developer.ts => developer.types.ts} | 0 src/types/{filter.ts => filter.types.ts} | 0 19 files changed, 22 insertions(+), 26 deletions(-) rename src/types/{auth.ts => auth.types.ts} (58%) rename src/types/{board.ts => board.types.ts} (100%) rename src/types/{community.ts => community.types.ts} (100%) rename src/types/{developer.ts => developer.types.ts} (100%) rename src/types/{filter.ts => filter.types.ts} (100%) diff --git a/src/components/common/PositionSelector.tsx b/src/components/common/PositionSelector.tsx index 262e754..d9d7e0f 100644 --- a/src/components/common/PositionSelector.tsx +++ b/src/components/common/PositionSelector.tsx @@ -9,7 +9,7 @@ import { import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { fetchPositions } from "@/lib/api"; -import type { PositionCategory } from "@/types/filter"; +import type { PositionCategory } from "@/types/filter.types"; interface PositionSelectorProps { selectedPosition: number | null; diff --git a/src/components/common/StackSelector.tsx b/src/components/common/StackSelector.tsx index 56382ba..fdcb202 100644 --- a/src/components/common/StackSelector.tsx +++ b/src/components/common/StackSelector.tsx @@ -17,7 +17,7 @@ import { } from "@/components/ui/popover"; import { Label } from "@/components/ui/label"; import { fetchStacks } from "@/lib/api"; -import type { Stack } from "@/types/filter"; +import type { Stack } from "@/types/filter.types"; interface StackSelectorProps { selectedStackIds: number[]; diff --git a/src/hooks/usePost.ts b/src/hooks/usePost.ts index 5dcef44..6b428e4 100644 --- a/src/hooks/usePost.ts +++ b/src/hooks/usePost.ts @@ -1,5 +1,5 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import type { Post, Comment } from "@/types/community"; +import type { Post, Comment } from "@/types/community.types"; const fetchPost = async (postId: string): Promise => { const res = await fetch(`/api/posts/${postId}`); diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index a4e6393..c5e3613 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -1,9 +1,13 @@ import { http, HttpResponse } from "msw"; -import type { Developer } from "@/types/developer"; -import type { Post } from "@/types/board"; +import type { Developer } from "@/types/developer.types"; +import type { Post } from "@/types/board.types"; import type { ExploreTabValue } from "@/constants/explore"; import type { UICategory } from "@/constants/board"; -import type { Post as CommunityPost, Comment, Reply } from "@/types/community"; +import type { + Post as CommunityPost, + Comment, + Reply, +} from "@/types/community.types"; const FAKE_DEVELOPERS: Developer[] = [ { diff --git a/src/pages/community/__components/CommentItem.tsx b/src/pages/community/__components/CommentItem.tsx index 9d67f5f..238a431 100644 --- a/src/pages/community/__components/CommentItem.tsx +++ b/src/pages/community/__components/CommentItem.tsx @@ -1,4 +1,4 @@ -import type { Comment } from "@/types/community"; +import type { Comment } from "@/types/community.types"; import { Button } from "@/components/ui/button"; import { Heart, MessageCircle } from "lucide-react"; import { ReplyItem } from "./ReplyItem"; diff --git a/src/pages/community/__components/CommentSection.tsx b/src/pages/community/__components/CommentSection.tsx index b5ec7e0..d6a64be 100644 --- a/src/pages/community/__components/CommentSection.tsx +++ b/src/pages/community/__components/CommentSection.tsx @@ -1,4 +1,4 @@ -import type { Comment } from "@/types/community"; +import type { Comment } from "@/types/community.types"; import { CommentItem } from "./CommentItem"; interface CommentSectionProps { diff --git a/src/pages/community/__components/PostContent.tsx b/src/pages/community/__components/PostContent.tsx index ed42d25..c261db6 100644 --- a/src/pages/community/__components/PostContent.tsx +++ b/src/pages/community/__components/PostContent.tsx @@ -1,4 +1,4 @@ -import type { Post } from "@/types/community"; +import type { Post } from "@/types/community.types"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { Button } from "@/components/ui/button"; diff --git a/src/pages/community/__components/ReplyItem.tsx b/src/pages/community/__components/ReplyItem.tsx index e500086..7167482 100644 --- a/src/pages/community/__components/ReplyItem.tsx +++ b/src/pages/community/__components/ReplyItem.tsx @@ -1,4 +1,4 @@ -import type { Reply } from "@/types/community"; +import type { Reply } from "@/types/community.types"; import { Button } from "@/components/ui/button"; import { Heart } from "lucide-react"; diff --git a/src/pages/explore/_components/DeveloperProfileCard.tsx b/src/pages/explore/_components/DeveloperProfileCard.tsx index dc729ae..f1c8edd 100644 --- a/src/pages/explore/_components/DeveloperProfileCard.tsx +++ b/src/pages/explore/_components/DeveloperProfileCard.tsx @@ -1,4 +1,4 @@ -import type { Developer } from "@/types/developer"; +import type { Developer } from "@/types/developer.types"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; diff --git a/src/pages/explore/_components/layout/GridView.tsx b/src/pages/explore/_components/layout/GridView.tsx index 16e007f..b26bfd8 100644 --- a/src/pages/explore/_components/layout/GridView.tsx +++ b/src/pages/explore/_components/layout/GridView.tsx @@ -1,4 +1,4 @@ -import type { Developer } from "@/types/developer"; +import type { Developer } from "@/types/developer.types"; import { DeveloperProfileCard } from "../DeveloperProfileCard"; interface GridViewProps { diff --git a/src/pages/explore/_components/layout/ListView.tsx b/src/pages/explore/_components/layout/ListView.tsx index ce44e51..813f4d8 100644 --- a/src/pages/explore/_components/layout/ListView.tsx +++ b/src/pages/explore/_components/layout/ListView.tsx @@ -1,4 +1,4 @@ -import type { Developer } from "@/types/developer"; +import type { Developer } from "@/types/developer.types"; import { DeveloperProfileCard } from "../DeveloperProfileCard"; interface ListViewProps { diff --git a/src/pages/explore/index.tsx b/src/pages/explore/index.tsx index 04cdb7b..588e2c5 100644 --- a/src/pages/explore/index.tsx +++ b/src/pages/explore/index.tsx @@ -3,7 +3,7 @@ import { Settings2, LayoutGrid, List } from "lucide-react"; import { Button } from "../../components/ui/button"; import { FilteringModal } from "./_components/FilteringModal"; import { EXPLORE_TABS, type ExploreTabValue } from "../../constants/explore"; -import type { Developer } from "../../types/developer"; +import type { Developer } from "../../types/developer.types"; import { ListView } from "./_components/layout/ListView"; import { GridView } from "./_components/layout/GridView"; import { AppTabs } from "../../components/common/AppTabs"; diff --git a/src/pages/home/_components/ListItem.tsx b/src/pages/home/_components/ListItem.tsx index 6e88562..5df1066 100644 --- a/src/pages/home/_components/ListItem.tsx +++ b/src/pages/home/_components/ListItem.tsx @@ -2,7 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Heart, MessageCircle } from "lucide-react"; import { useNavigate } from "react-router-dom"; -import type { Post } from "@/types/board"; +import type { Post } from "@/types/board.types"; import { BOARD_CATEGORIES } from "@/constants/board"; interface ListItemProps { diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index b571f96..857b880 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; import { Bell, Plus, Search } from "lucide-react"; import { useParams, useNavigate } from "react-router-dom"; -import type { Post } from "../../types/board"; +import type { Post } from "../../types/board.types"; import { fetchPosts } from "../../lib/api"; import { AppTabs } from "../../components/common/AppTabs"; import { AddPostModal } from "./_components/modal/AddPostModal"; diff --git a/src/types/auth.ts b/src/types/auth.types.ts similarity index 58% rename from src/types/auth.ts rename to src/types/auth.types.ts index 4e6d959..8d52e41 100644 --- a/src/types/auth.ts +++ b/src/types/auth.types.ts @@ -13,17 +13,9 @@ export interface User { blog?: string; } -export interface ExistingUserResponse { - status: 200; +export interface LoginResponse { accessToken: string; sessionId: string; - user: User; } -export interface NewUserResponse { - status: 210; - githubId: string; - user: User; -} - -export type FetchLoginResponse = ExistingUserResponse | NewUserResponse; \ No newline at end of file +export type FetchLoginResponse = LoginResponse; diff --git a/src/types/board.ts b/src/types/board.types.ts similarity index 100% rename from src/types/board.ts rename to src/types/board.types.ts diff --git a/src/types/community.ts b/src/types/community.types.ts similarity index 100% rename from src/types/community.ts rename to src/types/community.types.ts diff --git a/src/types/developer.ts b/src/types/developer.types.ts similarity index 100% rename from src/types/developer.ts rename to src/types/developer.types.ts diff --git a/src/types/filter.ts b/src/types/filter.types.ts similarity index 100% rename from src/types/filter.ts rename to src/types/filter.types.ts From 4deb912ccdf5073a33c8d480196e8d7282e40b86 Mon Sep 17 00:00:00 2001 From: Monixc Date: Thu, 7 Aug 2025 21:31:22 +0900 Subject: [PATCH 03/30] =?UTF-8?q?style:=20OnboardingStep1=20=EB=B0=8F=20On?= =?UTF-8?q?boardingStep2=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=9D=98?= =?UTF-8?q?=20=EC=B5=9C=EC=86=8C=20=EB=86=92=EC=9D=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/onboarding/__components/OnboardingStep1.tsx | 2 +- src/pages/onboarding/__components/OnboardingStep2.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/onboarding/__components/OnboardingStep1.tsx b/src/pages/onboarding/__components/OnboardingStep1.tsx index 28ffec4..93b43dc 100644 --- a/src/pages/onboarding/__components/OnboardingStep1.tsx +++ b/src/pages/onboarding/__components/OnboardingStep1.tsx @@ -62,7 +62,7 @@ export function OnboardingStep1({ onNext, data }: StepProps) { }; return ( -
+
diff --git a/src/pages/onboarding/__components/OnboardingStep2.tsx b/src/pages/onboarding/__components/OnboardingStep2.tsx index 88a9780..3e42aab 100644 --- a/src/pages/onboarding/__components/OnboardingStep2.tsx +++ b/src/pages/onboarding/__components/OnboardingStep2.tsx @@ -56,7 +56,7 @@ export function OnboardingStep2({ onNext, data }: StepProps) { }; return ( -
+
{/* 포지션 */} Date: Thu, 7 Aug 2025 23:35:55 +0900 Subject: [PATCH 04/30] =?UTF-8?q?refactor:=20api=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=ED=8C=85=20=EC=A1=B0=EA=B1=B4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useGitHubCallback.ts | 49 +++++++++++++++++----------------- src/lib/api.ts | 37 ++++++++++++------------- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/src/hooks/useGitHubCallback.ts b/src/hooks/useGitHubCallback.ts index 08f89bd..9746929 100644 --- a/src/hooks/useGitHubCallback.ts +++ b/src/hooks/useGitHubCallback.ts @@ -1,7 +1,7 @@ -import { useEffect, useRef } from 'react'; -import { useNavigate, useSearchParams } from 'react-router-dom'; -import { useMutation } from '@tanstack/react-query'; -import { fetchLogin } from '@/lib/api'; +import { useEffect, useRef } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { useMutation } from "@tanstack/react-query"; +import { fetchLogin } from "@/lib/api"; export const useGitHubCallback = () => { const [searchParams] = useSearchParams(); @@ -12,20 +12,19 @@ export const useGitHubCallback = () => { mutationFn: fetchLogin, retry: false, onSuccess: (result) => { - sessionStorage.removeItem('github_oauth_state'); + sessionStorage.removeItem("github_oauth_state"); - if (result.status === 200) { - localStorage.setItem('access_token', result.accessToken); - localStorage.setItem('session_id', result.sessionId); - navigate('/', { replace: true }); - } else if (result.status === 210) { - sessionStorage.setItem('github_id', result.githubId); - navigate('/onboarding', { replace: true }); + if (result.sessionId && !result.accessToken) { + navigate("/onboarding", { replace: true }); + } else if (result.accessToken) { + localStorage.setItem("access_token", result.accessToken); + localStorage.setItem("session_id", result.sessionId); + navigate("/board", { replace: true }); } }, onError: (error) => { - console.error('OAuth callback error:', error); - } + console.error("OAuth callback error:", error); + }, }); useEffect(() => { @@ -34,26 +33,26 @@ export const useGitHubCallback = () => { const handleCallback = () => { hasProcessed.current = true; - const code = searchParams.get('code'); - const error = searchParams.get('error'); - const state = searchParams.get('state'); + const code = searchParams.get("code"); + const error = searchParams.get("error"); + const state = searchParams.get("state"); if (error) { - navigate('/login', { replace: true }); - console.error('GitHub OAuth 에러:', error); + navigate("/login", { replace: true }); + console.error("GitHub OAuth 에러:", error); return; } if (!code) { - navigate('/login', { replace: true }); - console.log('GitHub에서 인증 코드를 받지 못했습니다'); + navigate("/login", { replace: true }); + console.log("GitHub에서 인증 코드를 받지 못했습니다"); return; } - const storedState = sessionStorage.getItem('github_oauth_state'); + const storedState = sessionStorage.getItem("github_oauth_state"); if (state !== storedState) { - navigate('/login', { replace: true }); - console.log('보안 검증 실패'); + navigate("/login", { replace: true }); + console.log("보안 검증 실패"); return; } @@ -68,4 +67,4 @@ export const useGitHubCallback = () => { isError: signInMutation.isError, error: signInMutation.error, }; -}; \ No newline at end of file +}; diff --git a/src/lib/api.ts b/src/lib/api.ts index bf82c9b..193ebf6 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,9 +1,9 @@ -import type { Post } from "@/types/board"; +import type { Post } from "@/types/board.types"; import type { UICategory } from "@/constants/board"; -import type { Developer } from "@/types/developer"; +import type { Developer } from "@/types/developer.types"; import type { ExploreTabValue } from "@/constants/explore"; -import type { PositionCategory, Stack } from "@/types/filter"; -import type { FetchLoginResponse } from "@/types/auth"; +import type { PositionCategory, Stack } from "@/types/filter.types"; +import type { FetchLoginResponse } from "@/types/auth.types"; export const fetchPosts = async (category: UICategory): Promise => { const response = await fetch(`/api/posts?category=${category}`); @@ -40,12 +40,17 @@ export const fetchStacks = async (): Promise => { }; export const fetchLogin = async (code: string): Promise => { - const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/auth/sign-in?code=${encodeURIComponent(code)}`, { - method: 'GET', - headers: { - 'Accept': 'application/json' - }, - }); + const response = await fetch( + `${ + import.meta.env.VITE_API_BASE_URL + }/auth/sign-in?code=${encodeURIComponent(code)}`, + { + method: "GET", + headers: { + Accept: "application/json", + }, + } + ); if (!response.ok) { throw new Error(`GitHub 로그인 실패: ${response.status}`); @@ -55,18 +60,10 @@ export const fetchLogin = async (code: string): Promise => { if (response.status === 200) { return { - status: 200, accessToken: data.accessToken, sessionId: data.sessionId, - user: data.user - }; - } else if (response.status === 210) { - return { - status: 210, - githubId: data.githubId, - user: data.user }; } else { - throw new Error(`예상하지 못한 응답 상태: ${response.status}`); + throw new Error(`GitHub 로그인 실패: ${response.status}`); } -}; \ No newline at end of file +}; From dd048c7c352281d10199cefc7a715e17e1e8f890 Mon Sep 17 00:00:00 2001 From: Monixc Date: Fri, 8 Aug 2025 03:25:41 +0900 Subject: [PATCH 05/30] =?UTF-8?q?chore:=20@use-funnel=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 75 +++++++++++++++++++++++++++++++++++++++++++++-- package.json | 2 ++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index c72f63a..c52c7de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,13 +20,14 @@ "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0", "@tanstack/react-query": "^5.83.0", - "@tanstack/react-query": "^5.83.0", + "@use-funnel/react-router-dom": "^0.0.14", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", "lucide-react": "^0.525.0", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-hot-toast": "^2.5.2", "react-router-dom": "^7.7.0", "tailwind-merge": "^3.3.1" }, @@ -1131,6 +1132,29 @@ } } }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slider": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.5.tgz", @@ -1961,6 +1985,28 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@use-funnel/core": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@use-funnel/core/-/core-0.0.12.tgz", + "integrity": "sha512-IikmJCqiQxbqxMAKdIEvX4MiuaM4nu0mfuAdnbI4CKf1FFLy/c4GHGKHW2+zm4aQAdEtxCuwou/wOwDhrTSWbg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/@use-funnel/react-router-dom": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@use-funnel/react-router-dom/-/react-router-dom-0.0.14.tgz", + "integrity": "sha512-bQtSvV8lqwDdytabVEkaL5H/LyckQwvnCyUQdY4lSeN1RXdXtQEyhUV0Z2bnigMkt0Qd6iOxIVn1zwoYMK6oEw==", + "license": "MIT", + "dependencies": { + "@use-funnel/core": "^0.0.12" + }, + "peerDependencies": { + "react": ">=16.8", + "react-router-dom": ">=6" + } + }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.10.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.2.tgz", @@ -2286,7 +2332,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, "license": "MIT" }, "node_modules/debug": { @@ -2762,6 +2807,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3439,6 +3493,23 @@ "react": "^19.1.0" } }, + "node_modules/react-hot-toast": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", + "integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-remove-scroll": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", diff --git a/package.json b/package.json index a3ae7d6..8f8e98b 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,14 @@ "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0", "@tanstack/react-query": "^5.83.0", + "@use-funnel/react-router-dom": "^0.0.14", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", "lucide-react": "^0.525.0", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-hot-toast": "^2.5.2", "react-router-dom": "^7.7.0", "tailwind-merge": "^3.3.1" }, From 755b3160bff039db8d2241e642d6645a9c850ba9 Mon Sep 17 00:00:00 2001 From: Monixc Date: Fri, 8 Aug 2025 03:26:19 +0900 Subject: [PATCH 06/30] =?UTF-8?q?refactor:=20OnboardingPage.tsx->Onboardin?= =?UTF-8?q?g/index.tsx=20=EB=B3=80=EA=B2=BD=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.tsx | 2 +- src/pages/onboarding/OnboardingPage.tsx | 95 -------- .../__components/OnboardingStep1.tsx | 55 +++-- .../__components/OnboardingStep2.tsx | 40 +++- .../__components/OnboardingStep3.tsx | 11 +- src/pages/onboarding/index.tsx | 219 ++++++++++++++++++ 6 files changed, 294 insertions(+), 128 deletions(-) delete mode 100644 src/pages/onboarding/OnboardingPage.tsx create mode 100644 src/pages/onboarding/index.tsx diff --git a/src/main.tsx b/src/main.tsx index 3615df3..bc85190 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -23,7 +23,7 @@ const queryClient = new QueryClient({ }, }, }); -import { OnboardingPage } from "./pages/onboarding/OnboardingPage.tsx"; +import { OnboardingPage } from "./pages/onboarding/index.tsx"; import { PostDetailPage } from "./pages/community/PostDetailPage.tsx"; import ChatPage from "./pages/chat/index.tsx"; import SearchPage from "./pages/search/index.tsx"; diff --git a/src/pages/onboarding/OnboardingPage.tsx b/src/pages/onboarding/OnboardingPage.tsx deleted file mode 100644 index 1165f9c..0000000 --- a/src/pages/onboarding/OnboardingPage.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useState } from "react"; -import { OnboardingStep1 } from "@/pages/onboarding/__components/OnboardingStep1"; -import { OnboardingStep2 } from "@/pages/onboarding/__components/OnboardingStep2"; -import { OnboardingStep3 } from "@/pages/onboarding/__components/OnboardingStep3"; -import { OnboardingStep4 } from "@/pages/onboarding/__components/OnboardingStep4"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import Header from "@/components/layout/Header"; -import { ArrowLeft } from "lucide-react"; - -export interface OnboardingData { - nickname?: string; - age?: number; - experience?: number; - bio?: string; - position?: number; - techStack?: number[]; - interests?: number[]; - linkedin?: string; - email?: string; - instagram?: string; - blog?: string; -} - -export function OnboardingPage() { - const [step, setStep] = useState(1); - const [onboardingData, setOnboardingData] = useState>( - {} - ); - const TOTAL_STEPS = 4; - - const handleNext = (currentStepData: Partial) => { - const newData = { ...onboardingData, ...currentStepData }; - setOnboardingData(newData); - - if (step < TOTAL_STEPS) { - setStep((prev) => prev + 1); - } else { - console.log("최종 데이터:", newData); - } - }; - - const progress = (step / TOTAL_STEPS) * 100; - - const getStepTitle = () => { - switch (step) { - case 1: - return "프로필 설정"; - case 2: - return "기술 / 관심 분야"; - case 3: - return "추가 정보"; - case 4: - return ""; - default: - return "프로필 설정"; - } - }; - - return ( -
-
-
- {step < TOTAL_STEPS && ( -
-
-
- )} - - {step < TOTAL_STEPS && ( - - - {getStepTitle()} - - - )} - - {step === 1 && ( - - )} - {step === 2 && ( - - )} - {step === 3 && ( - - )} - {step === 4 && } - - -
-
- ); -} diff --git a/src/pages/onboarding/__components/OnboardingStep1.tsx b/src/pages/onboarding/__components/OnboardingStep1.tsx index 93b43dc..e1b6b1c 100644 --- a/src/pages/onboarding/__components/OnboardingStep1.tsx +++ b/src/pages/onboarding/__components/OnboardingStep1.tsx @@ -1,5 +1,5 @@ -import { useState } from "react"; -import type { OnboardingData } from "@/pages/onboarding/OnboardingPage"; +import { useState, useEffect } from "react"; +import type { OnboardingData } from "@/pages/onboarding/index.tsx"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -27,6 +27,20 @@ export function OnboardingStep1({ onNext, data }: StepProps) { data.experience ? `${data.experience}년차` : "" ); const [bio, setBio] = useState(data.bio || ""); + const [isValid, setIsValid] = useState(false); + + // 유효성 검사 + useEffect(() => { + const isValidForm = + nickname.trim() !== "" && + age !== "" && + Number(age) > 0 && + Number(age) < MAX_AGE_LIMIT && + experience !== "" && + bio.trim() !== ""; + + setIsValid(isValidForm); + }, [nickname, age, experience, bio]); const handleAgeChange = (e: React.ChangeEvent) => { const value = e.target.value; @@ -39,26 +53,18 @@ export function OnboardingStep1({ onNext, data }: StepProps) { }; const handleSubmit = () => { - if (age === "") { - alert("나이를 입력해주세요."); - return; - } - if (Number(age) >= MAX_AGE_LIMIT) { - alert(`나이는 ${MAX_AGE_LIMIT} 미만으로 입력해주세요.`); - return; - } - if (age < 1) { - alert("나이를 올바르게 입력해주세요."); - return; - } - if (!nickname || !age || !experience || !bio) { - alert("모든 항목을 입력해주세요."); + if (!isValid) { + alert("모든 항목을 올바르게 입력해주세요."); return; } const experienceValue = parseInt(experience, 10); - - onNext({ nickname, age: Number(age), experience: experienceValue, bio }); + onNext({ + nickname: nickname.trim(), + age: Number(age), + experience: experienceValue, + bio: bio.trim(), + }); }; return ( @@ -89,6 +95,11 @@ export function OnboardingStep1({ onNext, data }: StepProps) { "border-red-500 focus:border-red-500 text-red-500" )} /> + {Number(age) >= MAX_AGE_LIMIT && ( +

+ 나이는 {MAX_AGE_LIMIT} 미만으로 입력해주세요. +

+ )}
@@ -122,7 +133,13 @@ export function OnboardingStep1({ onNext, data }: StepProps) {
diff --git a/src/pages/onboarding/__components/OnboardingStep2.tsx b/src/pages/onboarding/__components/OnboardingStep2.tsx index 3e42aab..853b475 100644 --- a/src/pages/onboarding/__components/OnboardingStep2.tsx +++ b/src/pages/onboarding/__components/OnboardingStep2.tsx @@ -1,10 +1,11 @@ -import { useState } from "react"; -import type { OnboardingData } from "@/pages/onboarding/OnboardingPage"; +import { useState, useEffect } from "react"; +import type { OnboardingData } from "@/pages/onboarding/index.tsx"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { Label } from "@/components/ui/label"; import { PositionSelector } from "@/components/common/PositionSelector"; import { StackSelector } from "@/components/common/StackSelector"; + const INTEREST_TOPICS = [ { id: 1, name: "업계 동향" }, { id: 2, name: "직무 정보" }, @@ -29,6 +30,17 @@ export function OnboardingStep2({ onNext, data }: StepProps) { const [selectedInterestIds, setSelectedInterestIds] = useState( data.interests || [] ); + const [isValid, setIsValid] = useState(false); + + // 유효성 검사 + useEffect(() => { + const isValidForm = + position !== undefined && + selectedStackIds.length > 0 && + selectedInterestIds.length > 0; + + setIsValid(isValidForm); + }, [position, selectedStackIds, selectedInterestIds]); const handleInterestToggle = (topicId: number) => { const newInterestIds = selectedInterestIds.includes(topicId) @@ -40,16 +52,13 @@ export function OnboardingStep2({ onNext, data }: StepProps) { }; const handleSubmit = () => { - if (!position) { - alert("포지션을 선택해주세요."); - return; - } - if (selectedInterestIds.length === 0) { - alert("관심 분야를 1개 이상 선택해주세요."); + if (!isValid) { + alert("포지션, 기술 스택, 관심 분야를 모두 선택해주세요."); return; } + onNext({ - position: position, + position: position!, techStack: selectedStackIds, interests: selectedInterestIds, }); @@ -87,13 +96,24 @@ export function OnboardingStep2({ onNext, data }: StepProps) { ))}
+ {selectedInterestIds.length === 0 && ( +

+ 관심 분야를 1개 이상 선택해주세요. +

+ )}
diff --git a/src/pages/onboarding/__components/OnboardingStep3.tsx b/src/pages/onboarding/__components/OnboardingStep3.tsx index e0ba646..72e0407 100644 --- a/src/pages/onboarding/__components/OnboardingStep3.tsx +++ b/src/pages/onboarding/__components/OnboardingStep3.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import type { OnboardingData } from "@/pages/onboarding/OnboardingPage"; +import type { OnboardingData } from "@/pages/onboarding/index.tsx"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -17,7 +17,12 @@ export function OnboardingStep3({ onNext, data }: StepProps) { const [blog, setBlog] = useState(data.blog || ""); const handleSubmit = () => { - onNext({ linkedin, email, instagram, blog }); + onNext({ + linkedin: linkedin.trim(), + email: email.trim(), + instagram: instagram.trim(), + blog: blog.trim(), + }); }; return ( @@ -87,7 +92,7 @@ export function OnboardingStep3({ onNext, data }: StepProps) {
diff --git a/src/pages/onboarding/index.tsx b/src/pages/onboarding/index.tsx new file mode 100644 index 0000000..c3ef075 --- /dev/null +++ b/src/pages/onboarding/index.tsx @@ -0,0 +1,219 @@ +import { useFunnel } from "@use-funnel/react-router-dom"; +import { OnboardingStep1 } from "@/pages/onboarding/__components/OnboardingStep1"; +import { OnboardingStep2 } from "@/pages/onboarding/__components/OnboardingStep2"; +import { OnboardingStep3 } from "@/pages/onboarding/__components/OnboardingStep3"; +import { OnboardingStep4 } from "@/pages/onboarding/__components/OnboardingStep4"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import Header from "@/components/layout/Header"; +import { ArrowLeft } from "lucide-react"; + +type ProfileSetup = { + nickname?: string; + age?: number; + experience?: number; + bio?: string; + location?: { + lng: number; + lat: number; + }; +}; + +type TechInterests = { + nickname: string; + age: number; + experience: number; + bio: string; + location: { + lng: number; + lat: number; + }; + position?: number; + techStack?: number[]; + interests?: number[]; +}; + +type AdditionalInfo = { + nickname: string; + age: number; + experience: number; + bio: string; + location: { + lng: number; + lat: number; + }; + position: number; + techStack: number[]; + interests: number[]; + linkedin?: string; + email?: string; + instagram?: string; + blog?: string; +}; + +type Complete = { + nickname: string; + age: number; + experience: number; + bio: string; + location: { + lng: number; + lat: number; + }; + position: number; + techStack: number[]; + interests: number[]; + linkedin?: string; + email?: string; + instagram?: string; + blog?: string; +}; + +export interface OnboardingData { + nickname?: string; + age?: number; + experience?: number; + bio?: string; + location?: { + lng: number; + lat: number; + }; + position?: number; + techStack?: number[]; + interests?: number[]; + linkedin?: string; + email?: string; + instagram?: string; + blog?: string; +} + +export function OnboardingPage() { + const funnel = useFunnel<{ + ProfileSetup: ProfileSetup; + TechInterests: TechInterests; + AdditionalInfo: AdditionalInfo; + Complete: Complete; + }>({ + id: "onboarding", + initial: { + step: "ProfileSetup", + context: {}, + }, + }); + + const getStepTitle = () => { + switch (funnel.step) { + case "ProfileSetup": + return "프로필 설정"; + case "TechInterests": + return "기술 / 관심 분야"; + case "AdditionalInfo": + return "추가 정보"; + case "Complete": + default: + return ""; + } + }; + + const getProgress = () => { + const stepOrder = [ + "ProfileSetup", + "TechInterests", + "AdditionalInfo", + "Complete", + ]; + const currentIndex = stepOrder.indexOf(funnel.step); + return ((currentIndex + 1) / stepOrder.length) * 100; + }; + + const canGoBack = + funnel.step !== "ProfileSetup" && funnel.step !== "Complete"; + + const handleBack = () => { + if (canGoBack) { + funnel.history.back(); + } + }; + + return ( +
+
: undefined} + onLeftClick={canGoBack ? handleBack : undefined} + /> +
+ {funnel.step !== "Complete" && ( +
+
+
+ )} + + {funnel.step !== "Complete" && ( + + + {getStepTitle()} + + + )} + + ( + { + if ( + data.nickname && + data.age && + data.experience && + data.bio && + data.location + ) { + history.push("TechInterests", (prev) => ({ + ...prev, + nickname: data.nickname!, + age: data.age!, + experience: data.experience!, + bio: data.bio!, + location: data.location!, + })); + } + }} + data={funnel.context} + /> + )} + TechInterests={({ context, history }) => ( + { + if (data.position && data.techStack && data.interests) { + history.push("AdditionalInfo", (prev) => ({ + ...prev, + position: data.position!, + techStack: data.techStack!, + interests: data.interests!, + })); + } + }} + data={context} + /> + )} + AdditionalInfo={({ context, history }) => ( + { + history.push("Complete", (prev) => ({ + ...prev, + ...data, + })); + }} + data={context} + /> + )} + Complete={() => } + /> + + +
+
+ ); +} From 7d024e973ce6533cd96651b3654429341dfb9b15 Mon Sep 17 00:00:00 2001 From: Monixc Date: Fri, 8 Aug 2025 03:26:50 +0900 Subject: [PATCH 07/30] =?UTF-8?q?refactor:=20OnboardingPage=20import=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.tsx b/src/main.tsx index 3615df3..bc85190 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -23,7 +23,7 @@ const queryClient = new QueryClient({ }, }, }); -import { OnboardingPage } from "./pages/onboarding/OnboardingPage.tsx"; +import { OnboardingPage } from "./pages/onboarding/index.tsx"; import { PostDetailPage } from "./pages/community/PostDetailPage.tsx"; import ChatPage from "./pages/chat/index.tsx"; import SearchPage from "./pages/search/index.tsx"; From d52cc6636ac79081e5d9e076bc770122683ea5a6 Mon Sep 17 00:00:00 2001 From: Monixc Date: Fri, 8 Aug 2025 03:29:57 +0900 Subject: [PATCH 08/30] =?UTF-8?q?feat:=20@use-funnel=EB=A1=9C=20=EC=98=A8?= =?UTF-8?q?=EB=B3=B4=EB=94=A9=20=ED=8C=A8=EC=9D=B4=EC=A7=80=20=ED=8D=BC?= =?UTF-8?q?=EB=84=90=20=ED=8C=A8=ED=84=B4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/onboarding/OnboardingPage.tsx | 95 -------- .../__components/OnboardingStep1.tsx | 55 +++-- .../__components/OnboardingStep2.tsx | 35 ++- .../__components/OnboardingStep3.tsx | 11 +- src/pages/onboarding/index.tsx | 219 ++++++++++++++++++ 5 files changed, 288 insertions(+), 127 deletions(-) delete mode 100644 src/pages/onboarding/OnboardingPage.tsx create mode 100644 src/pages/onboarding/index.tsx diff --git a/src/pages/onboarding/OnboardingPage.tsx b/src/pages/onboarding/OnboardingPage.tsx deleted file mode 100644 index 1165f9c..0000000 --- a/src/pages/onboarding/OnboardingPage.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useState } from "react"; -import { OnboardingStep1 } from "@/pages/onboarding/__components/OnboardingStep1"; -import { OnboardingStep2 } from "@/pages/onboarding/__components/OnboardingStep2"; -import { OnboardingStep3 } from "@/pages/onboarding/__components/OnboardingStep3"; -import { OnboardingStep4 } from "@/pages/onboarding/__components/OnboardingStep4"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import Header from "@/components/layout/Header"; -import { ArrowLeft } from "lucide-react"; - -export interface OnboardingData { - nickname?: string; - age?: number; - experience?: number; - bio?: string; - position?: number; - techStack?: number[]; - interests?: number[]; - linkedin?: string; - email?: string; - instagram?: string; - blog?: string; -} - -export function OnboardingPage() { - const [step, setStep] = useState(1); - const [onboardingData, setOnboardingData] = useState>( - {} - ); - const TOTAL_STEPS = 4; - - const handleNext = (currentStepData: Partial) => { - const newData = { ...onboardingData, ...currentStepData }; - setOnboardingData(newData); - - if (step < TOTAL_STEPS) { - setStep((prev) => prev + 1); - } else { - console.log("최종 데이터:", newData); - } - }; - - const progress = (step / TOTAL_STEPS) * 100; - - const getStepTitle = () => { - switch (step) { - case 1: - return "프로필 설정"; - case 2: - return "기술 / 관심 분야"; - case 3: - return "추가 정보"; - case 4: - return ""; - default: - return "프로필 설정"; - } - }; - - return ( -
-
-
- {step < TOTAL_STEPS && ( -
-
-
- )} - - {step < TOTAL_STEPS && ( - - - {getStepTitle()} - - - )} - - {step === 1 && ( - - )} - {step === 2 && ( - - )} - {step === 3 && ( - - )} - {step === 4 && } - - -
-
- ); -} diff --git a/src/pages/onboarding/__components/OnboardingStep1.tsx b/src/pages/onboarding/__components/OnboardingStep1.tsx index 93b43dc..2ae2003 100644 --- a/src/pages/onboarding/__components/OnboardingStep1.tsx +++ b/src/pages/onboarding/__components/OnboardingStep1.tsx @@ -1,5 +1,5 @@ -import { useState } from "react"; -import type { OnboardingData } from "@/pages/onboarding/OnboardingPage"; +import { useState, useEffect } from "react"; +import type { OnboardingData } from "@/pages/onboarding/index.tsx"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -27,6 +27,20 @@ export function OnboardingStep1({ onNext, data }: StepProps) { data.experience ? `${data.experience}년차` : "" ); const [bio, setBio] = useState(data.bio || ""); + const [isValid, setIsValid] = useState(false); + + // 유효성 검사 + useEffect(() => { + const isValidForm = + nickname.trim() !== "" && + age !== "" && + Number(age) > 0 && + Number(age) < MAX_AGE_LIMIT && + experience !== "" && + bio.trim() !== ""; + + setIsValid(isValidForm); + }, [nickname, age, experience, bio]); const handleAgeChange = (e: React.ChangeEvent) => { const value = e.target.value; @@ -39,26 +53,18 @@ export function OnboardingStep1({ onNext, data }: StepProps) { }; const handleSubmit = () => { - if (age === "") { - alert("나이를 입력해주세요."); - return; - } - if (Number(age) >= MAX_AGE_LIMIT) { - alert(`나이는 ${MAX_AGE_LIMIT} 미만으로 입력해주세요.`); - return; - } - if (age < 1) { - alert("나이를 올바르게 입력해주세요."); - return; - } - if (!nickname || !age || !experience || !bio) { - alert("모든 항목을 입력해주세요."); + if (!isValid) { + alert("모든 항목을 올바르게 입력해주세요."); return; } const experienceValue = parseInt(experience, 10); - - onNext({ nickname, age: Number(age), experience: experienceValue, bio }); + onNext({ + nickname: nickname.trim(), + age: Number(age), + experience: experienceValue, + bio: bio.trim(), + }); }; return ( @@ -89,6 +95,11 @@ export function OnboardingStep1({ onNext, data }: StepProps) { "border-red-500 focus:border-red-500 text-red-500" )} /> + {Number(age) >= MAX_AGE_LIMIT && ( +

+ 나이는 {MAX_AGE_LIMIT} 미만으로 입력해주세요. +

+ )}
@@ -122,7 +133,13 @@ export function OnboardingStep1({ onNext, data }: StepProps) {
diff --git a/src/pages/onboarding/__components/OnboardingStep2.tsx b/src/pages/onboarding/__components/OnboardingStep2.tsx index 3e42aab..ffd6410 100644 --- a/src/pages/onboarding/__components/OnboardingStep2.tsx +++ b/src/pages/onboarding/__components/OnboardingStep2.tsx @@ -1,10 +1,11 @@ -import { useState } from "react"; -import type { OnboardingData } from "@/pages/onboarding/OnboardingPage"; +import { useState, useEffect } from "react"; +import type { OnboardingData } from "@/pages/onboarding/index.tsx"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { Label } from "@/components/ui/label"; import { PositionSelector } from "@/components/common/PositionSelector"; import { StackSelector } from "@/components/common/StackSelector"; + const INTEREST_TOPICS = [ { id: 1, name: "업계 동향" }, { id: 2, name: "직무 정보" }, @@ -29,6 +30,17 @@ export function OnboardingStep2({ onNext, data }: StepProps) { const [selectedInterestIds, setSelectedInterestIds] = useState( data.interests || [] ); + const [isValid, setIsValid] = useState(false); + + // 유효성 검사 + useEffect(() => { + const isValidForm = + position !== undefined && + selectedStackIds.length > 0 && + selectedInterestIds.length > 0; + + setIsValid(isValidForm); + }, [position, selectedStackIds, selectedInterestIds]); const handleInterestToggle = (topicId: number) => { const newInterestIds = selectedInterestIds.includes(topicId) @@ -40,16 +52,13 @@ export function OnboardingStep2({ onNext, data }: StepProps) { }; const handleSubmit = () => { - if (!position) { - alert("포지션을 선택해주세요."); - return; - } - if (selectedInterestIds.length === 0) { - alert("관심 분야를 1개 이상 선택해주세요."); + if (!isValid) { + alert("포지션, 기술 스택, 관심 분야를 모두 선택해주세요."); return; } + onNext({ - position: position, + position: position!, techStack: selectedStackIds, interests: selectedInterestIds, }); @@ -93,7 +102,13 @@ export function OnboardingStep2({ onNext, data }: StepProps) {
diff --git a/src/pages/onboarding/__components/OnboardingStep3.tsx b/src/pages/onboarding/__components/OnboardingStep3.tsx index e0ba646..72e0407 100644 --- a/src/pages/onboarding/__components/OnboardingStep3.tsx +++ b/src/pages/onboarding/__components/OnboardingStep3.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import type { OnboardingData } from "@/pages/onboarding/OnboardingPage"; +import type { OnboardingData } from "@/pages/onboarding/index.tsx"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -17,7 +17,12 @@ export function OnboardingStep3({ onNext, data }: StepProps) { const [blog, setBlog] = useState(data.blog || ""); const handleSubmit = () => { - onNext({ linkedin, email, instagram, blog }); + onNext({ + linkedin: linkedin.trim(), + email: email.trim(), + instagram: instagram.trim(), + blog: blog.trim(), + }); }; return ( @@ -87,7 +92,7 @@ export function OnboardingStep3({ onNext, data }: StepProps) {
diff --git a/src/pages/onboarding/index.tsx b/src/pages/onboarding/index.tsx new file mode 100644 index 0000000..c3ef075 --- /dev/null +++ b/src/pages/onboarding/index.tsx @@ -0,0 +1,219 @@ +import { useFunnel } from "@use-funnel/react-router-dom"; +import { OnboardingStep1 } from "@/pages/onboarding/__components/OnboardingStep1"; +import { OnboardingStep2 } from "@/pages/onboarding/__components/OnboardingStep2"; +import { OnboardingStep3 } from "@/pages/onboarding/__components/OnboardingStep3"; +import { OnboardingStep4 } from "@/pages/onboarding/__components/OnboardingStep4"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import Header from "@/components/layout/Header"; +import { ArrowLeft } from "lucide-react"; + +type ProfileSetup = { + nickname?: string; + age?: number; + experience?: number; + bio?: string; + location?: { + lng: number; + lat: number; + }; +}; + +type TechInterests = { + nickname: string; + age: number; + experience: number; + bio: string; + location: { + lng: number; + lat: number; + }; + position?: number; + techStack?: number[]; + interests?: number[]; +}; + +type AdditionalInfo = { + nickname: string; + age: number; + experience: number; + bio: string; + location: { + lng: number; + lat: number; + }; + position: number; + techStack: number[]; + interests: number[]; + linkedin?: string; + email?: string; + instagram?: string; + blog?: string; +}; + +type Complete = { + nickname: string; + age: number; + experience: number; + bio: string; + location: { + lng: number; + lat: number; + }; + position: number; + techStack: number[]; + interests: number[]; + linkedin?: string; + email?: string; + instagram?: string; + blog?: string; +}; + +export interface OnboardingData { + nickname?: string; + age?: number; + experience?: number; + bio?: string; + location?: { + lng: number; + lat: number; + }; + position?: number; + techStack?: number[]; + interests?: number[]; + linkedin?: string; + email?: string; + instagram?: string; + blog?: string; +} + +export function OnboardingPage() { + const funnel = useFunnel<{ + ProfileSetup: ProfileSetup; + TechInterests: TechInterests; + AdditionalInfo: AdditionalInfo; + Complete: Complete; + }>({ + id: "onboarding", + initial: { + step: "ProfileSetup", + context: {}, + }, + }); + + const getStepTitle = () => { + switch (funnel.step) { + case "ProfileSetup": + return "프로필 설정"; + case "TechInterests": + return "기술 / 관심 분야"; + case "AdditionalInfo": + return "추가 정보"; + case "Complete": + default: + return ""; + } + }; + + const getProgress = () => { + const stepOrder = [ + "ProfileSetup", + "TechInterests", + "AdditionalInfo", + "Complete", + ]; + const currentIndex = stepOrder.indexOf(funnel.step); + return ((currentIndex + 1) / stepOrder.length) * 100; + }; + + const canGoBack = + funnel.step !== "ProfileSetup" && funnel.step !== "Complete"; + + const handleBack = () => { + if (canGoBack) { + funnel.history.back(); + } + }; + + return ( +
+
: undefined} + onLeftClick={canGoBack ? handleBack : undefined} + /> +
+ {funnel.step !== "Complete" && ( +
+
+
+ )} + + {funnel.step !== "Complete" && ( + + + {getStepTitle()} + + + )} + + ( + { + if ( + data.nickname && + data.age && + data.experience && + data.bio && + data.location + ) { + history.push("TechInterests", (prev) => ({ + ...prev, + nickname: data.nickname!, + age: data.age!, + experience: data.experience!, + bio: data.bio!, + location: data.location!, + })); + } + }} + data={funnel.context} + /> + )} + TechInterests={({ context, history }) => ( + { + if (data.position && data.techStack && data.interests) { + history.push("AdditionalInfo", (prev) => ({ + ...prev, + position: data.position!, + techStack: data.techStack!, + interests: data.interests!, + })); + } + }} + data={context} + /> + )} + AdditionalInfo={({ context, history }) => ( + { + history.push("Complete", (prev) => ({ + ...prev, + ...data, + })); + }} + data={context} + /> + )} + Complete={() => } + /> + + +
+
+ ); +} From 706fc17fc72e8a7a005520060900d5942c93eb5c Mon Sep 17 00:00:00 2001 From: Monixc Date: Fri, 8 Aug 2025 05:43:10 +0900 Subject: [PATCH 09/30] =?UTF-8?q?file:=20=EC=A2=85=EC=86=8D=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=ED=8F=B4=EB=8D=94=EB=AA=85=20=5F?= =?UTF-8?q?components=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OnboardingStep1.tsx | 24 +++++++++++++++++-- .../OnboardingStep2.tsx | 0 .../OnboardingStep3.tsx | 0 .../OnboardingStep4.tsx | 0 src/pages/onboarding/index.tsx | 8 +++---- 5 files changed, 26 insertions(+), 6 deletions(-) rename src/pages/onboarding/{__components => _components}/OnboardingStep1.tsx (86%) rename src/pages/onboarding/{__components => _components}/OnboardingStep2.tsx (100%) rename src/pages/onboarding/{__components => _components}/OnboardingStep3.tsx (100%) rename src/pages/onboarding/{__components => _components}/OnboardingStep4.tsx (100%) diff --git a/src/pages/onboarding/__components/OnboardingStep1.tsx b/src/pages/onboarding/_components/OnboardingStep1.tsx similarity index 86% rename from src/pages/onboarding/__components/OnboardingStep1.tsx rename to src/pages/onboarding/_components/OnboardingStep1.tsx index 2ae2003..0e94708 100644 --- a/src/pages/onboarding/__components/OnboardingStep1.tsx +++ b/src/pages/onboarding/_components/OnboardingStep1.tsx @@ -12,6 +12,7 @@ import { SelectValue, } from "@/components/ui/select"; import { cn } from "@/lib/utils"; +import { LocationSelector } from "@/components/common/LocationSelector"; const MAX_AGE_LIMIT = 120; @@ -27,6 +28,9 @@ export function OnboardingStep1({ onNext, data }: StepProps) { data.experience ? `${data.experience}년차` : "" ); const [bio, setBio] = useState(data.bio || ""); + const [location, setLocation] = useState<{ lng: number; lat: number } | null>( + data.location || null + ); const [isValid, setIsValid] = useState(false); // 유효성 검사 @@ -37,10 +41,11 @@ export function OnboardingStep1({ onNext, data }: StepProps) { Number(age) > 0 && Number(age) < MAX_AGE_LIMIT && experience !== "" && - bio.trim() !== ""; + bio.trim() !== "" && + location !== null; setIsValid(isValidForm); - }, [nickname, age, experience, bio]); + }, [nickname, age, experience, bio, location]); const handleAgeChange = (e: React.ChangeEvent) => { const value = e.target.value; @@ -52,6 +57,11 @@ export function OnboardingStep1({ onNext, data }: StepProps) { } }; + const handleLocationChange = (newLocation: { lng: number; lat: number }) => { + console.log("📍 온보딩에서 위치 변경:", newLocation); + setLocation(newLocation); + }; + const handleSubmit = () => { if (!isValid) { alert("모든 항목을 올바르게 입력해주세요."); @@ -64,6 +74,7 @@ export function OnboardingStep1({ onNext, data }: StepProps) { age: Number(age), experience: experienceValue, bio: bio.trim(), + location: location!, }); }; @@ -129,6 +140,15 @@ export function OnboardingStep1({ onNext, data }: StepProps) { className="bg-brand-surface border-transparent focus:!ring-0 focus:!border-brand-primary h-[120px] resize-none" />
+ + {/* 위치 정보 수집 섹션 */} +
+ + +
+
+ )} + + {status === "loading" && ( +
+
+ + 위치 정보 수집 중... +
+

+ GPS 신호를 받고 있습니다. 잠시만 기다려주세요. +

+
+ )} + + {status === "success" && location && ( +
+
+ + + 위치 정보 수집 완료 + +
+ {location.address ? ( +
+

+ 현재 위치: {location.address} +

+

+ * 약간의 오차가 있을 수 있습니다. +

+
+ ) : ( +

+ 위도: {location.lat.toFixed(6)}, 경도: {location.lng.toFixed(6)} +

+ )} + +
+ )} + + {status === "error" && ( +
+
+ + + 위치 정보 오류 + +
+

+ 위치 정보를 가져오는 중 오류가 발생했습니다. +

+ +
+ )} +
+ ); +}; diff --git a/src/hooks/queries/useLocationQuery.ts b/src/hooks/queries/useLocationQuery.ts new file mode 100644 index 0000000..8c0c35a --- /dev/null +++ b/src/hooks/queries/useLocationQuery.ts @@ -0,0 +1,88 @@ +import { useQuery } from "@tanstack/react-query"; +import { getAddressFromCoordinates } from "@/lib/location"; + +interface LocationData { + lng: number; + lat: number; +} + +interface LocationWithAddress extends LocationData { + address: string; +} + +const getCurrentLocation = (): Promise => { + return new Promise((resolve, reject) => { + if (!navigator.geolocation) { + reject(new Error("브라우저가 위치 정보를 지원하지 않습니다.")); + return; + } + + navigator.geolocation.getCurrentPosition( + (position) => { + const coords = { + lng: position.coords.longitude, + lat: position.coords.latitude, + }; + resolve(coords); + }, + (error) => { + reject(error); + }, + { + enableHighAccuracy: false, + timeout: 300000, + maximumAge: 300000, + } + ); + }); +}; + +const getLocationWithAddress = async ( + location: LocationData +): Promise => { + try { + const address = await getAddressFromCoordinates(location.lat, location.lng); + return { + ...location, + address, + }; + } catch (error) { + console.warn("주소 변환 실패:", error); + return { + ...location, + address: "", + }; + } +}; + +export const useLocationQuery = () => { + const locationQuery = useQuery({ + queryKey: ["location"], + queryFn: async () => { + const location = await getCurrentLocation(); + return getLocationWithAddress(location); + }, + enabled: false, + staleTime: 0, + gcTime: 0, + retry: 1, + retryDelay: 1000, + }); + + const refetchLocation = async () => { + try { + await locationQuery.refetch(); + } catch (error) { + console.error("위치 새로고침 실패:", error); + } + }; + + return { + location: locationQuery.data, + isLoading: locationQuery.isLoading || locationQuery.isFetching, + isError: locationQuery.isError, + error: locationQuery.error, + refetchLocation, + isUpdating: locationQuery.isFetching, + }; +}; diff --git a/src/lib/location.ts b/src/lib/location.ts new file mode 100644 index 0000000..987445d --- /dev/null +++ b/src/lib/location.ts @@ -0,0 +1,50 @@ +export const formatKoreanAddress = (displayName: string): string => { + const addressParts = displayName.split(", "); + + const koreanParts = addressParts.filter( + (part: string) => + part.includes("시") || + part.includes("구") || + part.includes("로") || + part.includes("길") + ); + + const sortedParts = koreanParts.sort((a, b) => { + const getPriority = (part: string) => { + if (part.includes("시")) return 1; + if (part.includes("구")) return 2; + if (part.includes("로") || part.includes("길")) return 3; + return 4; + }; + + return getPriority(a) - getPriority(b); + }); + + return sortedParts.slice(0, 3).join(" "); +}; + +export const getAddressFromCoordinates = async ( + lat: number, + lng: number +): Promise => { + try { + const response = await fetch( + `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&addressdetails=1&accept-language=ko` + ); + + if (!response.ok) { + throw new Error("주소 변환 실패"); + } + + const data = await response.json(); + + if (data.display_name) { + return formatKoreanAddress(data.display_name); + } else { + return "주소를 찾을 수 없습니다"; + } + } catch (error) { + console.error("주소 변환 오류:", error); + return "주소 변환 실패"; + } +}; diff --git a/src/pages/onboarding/_components/OnboardingStep1.tsx b/src/pages/onboarding/_components/OnboardingStep1.tsx index 0e94708..13e45b9 100644 --- a/src/pages/onboarding/_components/OnboardingStep1.tsx +++ b/src/pages/onboarding/_components/OnboardingStep1.tsx @@ -33,7 +33,6 @@ export function OnboardingStep1({ onNext, data }: StepProps) { ); const [isValid, setIsValid] = useState(false); - // 유효성 검사 useEffect(() => { const isValidForm = nickname.trim() !== "" && @@ -58,7 +57,6 @@ export function OnboardingStep1({ onNext, data }: StepProps) { }; const handleLocationChange = (newLocation: { lng: number; lat: number }) => { - console.log("📍 온보딩에서 위치 변경:", newLocation); setLocation(newLocation); }; @@ -141,7 +139,6 @@ export function OnboardingStep1({ onNext, data }: StepProps) { />
- {/* 위치 정보 수집 섹션 */}
Date: Fri, 8 Aug 2025 07:54:54 +0900 Subject: [PATCH 12/30] =?UTF-8?q?feat:=20msw=20=EB=B9=84=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.tsx | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index bc85190..d13da4a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,7 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import "./index.css"; import App from "./App.tsx"; import { LoginPage } from "./pages/auth/LoginPage.tsx"; @@ -8,8 +9,11 @@ import { BoardPage } from "./pages/home/index.tsx"; import { MyPage } from "./pages/my/index.tsx"; import { DeveloperPage } from "./pages/developer/index.tsx"; import { ExplorePage } from "./pages/explore/index.tsx"; +import { OnboardingPage } from "./pages/onboarding/index.tsx"; +import { PostDetailPage } from "./pages/community/PostDetailPage.tsx"; import AuthCallbackPage from "./pages/auth/AuthCallbackPage.tsx"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import ChatPage from "./pages/chat/index.tsx"; +import SearchPage from "./pages/search/index.tsx"; const queryClient = new QueryClient({ defaultOptions: { @@ -23,17 +27,6 @@ const queryClient = new QueryClient({ }, }, }); -import { OnboardingPage } from "./pages/onboarding/index.tsx"; -import { PostDetailPage } from "./pages/community/PostDetailPage.tsx"; -import ChatPage from "./pages/chat/index.tsx"; -import SearchPage from "./pages/search/index.tsx"; - -async function enableMocking() { - if (import.meta.env.DEV) { - const { worker } = await import("./mocks/browser.ts"); - return worker.start(); - } -} const router = createBrowserRouter([ { @@ -92,12 +85,10 @@ const router = createBrowserRouter([ }, ]); -enableMocking().then(() => { - createRoot(document.getElementById("root")!).render( - - - - - - ); -}); +createRoot(document.getElementById("root")!).render( + + + + + +); From 92350584879350e3410c898ccfd7a1e27ac61952 Mon Sep 17 00:00:00 2001 From: Monixc Date: Fri, 8 Aug 2025 09:27:15 +0900 Subject: [PATCH 13/30] =?UTF-8?q?fix:=20main,=20app=EC=97=90=EC=84=9C=20Qu?= =?UTF-8?q?eryClientProvider=20=EC=A4=91=EB=B3=B5=20=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b9cbc2a..965b01a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,11 @@ import { Outlet } from "react-router-dom"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import GlobalLayout from "./components/layout/GlobalLayout"; -const queryClient = new QueryClient(); - function App() { return ( - - - - - + + + ); } From 9da0c4f00289e0b9625cd3200174f541233d2fc1 Mon Sep 17 00:00:00 2001 From: Monixc Date: Fri, 8 Aug 2025 09:46:12 +0900 Subject: [PATCH 14/30] =?UTF-8?q?type:=20auth,=20tags=20api=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=83=80=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/api.ts | 69 --------------------------------------- src/types/auth.types.ts | 34 +++++++++++-------- src/types/filter.types.ts | 15 --------- src/types/tags.types.ts | 20 ++++++++++++ 4 files changed, 41 insertions(+), 97 deletions(-) delete mode 100644 src/lib/api.ts delete mode 100644 src/types/filter.types.ts create mode 100644 src/types/tags.types.ts diff --git a/src/lib/api.ts b/src/lib/api.ts deleted file mode 100644 index 193ebf6..0000000 --- a/src/lib/api.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { Post } from "@/types/board.types"; -import type { UICategory } from "@/constants/board"; -import type { Developer } from "@/types/developer.types"; -import type { ExploreTabValue } from "@/constants/explore"; -import type { PositionCategory, Stack } from "@/types/filter.types"; -import type { FetchLoginResponse } from "@/types/auth.types"; - -export const fetchPosts = async (category: UICategory): Promise => { - const response = await fetch(`/api/posts?category=${category}`); - if (!response.ok) { - throw new Error("게시물 목록을 불러오는 데 실패했습니다."); - } - return response.json(); -}; - -export const fetchDevelopers = async ( - category: ExploreTabValue -): Promise => { - const response = await fetch(`/api/developers?category=${category}`); - if (!response.ok) { - throw new Error("개발자 목록을 불러오는 데 실패했습니다."); - } - return response.json(); -}; - -export const fetchPositions = async (): Promise => { - const response = await fetch("/api/positions"); - if (!response.ok) { - throw new Error("포지션 목록을 불러오는 데 실패했습니다."); - } - return response.json(); -}; - -export const fetchStacks = async (): Promise => { - const response = await fetch("/api/stacks"); - if (!response.ok) { - throw new Error("기술 스택 목록을 불러오는 데 실패했습니다."); - } - return response.json(); -}; - -export const fetchLogin = async (code: string): Promise => { - const response = await fetch( - `${ - import.meta.env.VITE_API_BASE_URL - }/auth/sign-in?code=${encodeURIComponent(code)}`, - { - method: "GET", - headers: { - Accept: "application/json", - }, - } - ); - - if (!response.ok) { - throw new Error(`GitHub 로그인 실패: ${response.status}`); - } - - const data = await response.json(); - - if (response.status === 200) { - return { - accessToken: data.accessToken, - sessionId: data.sessionId, - }; - } else { - throw new Error(`GitHub 로그인 실패: ${response.status}`); - } -}; diff --git a/src/types/auth.types.ts b/src/types/auth.types.ts index 8d52e41..5b783d8 100644 --- a/src/types/auth.types.ts +++ b/src/types/auth.types.ts @@ -1,21 +1,29 @@ -export interface User { +export interface LoginResponse { + accessToken?: string; + sessionId?: string; + result?: { + nickname: string; + avatar: string; + accessToken: string; + }; +} + +export type FetchLoginResponse = LoginResponse; + +export interface SignUpRequest { nickname: string; age: number; experience: number; bio: string; - position: number; - techStack: number[]; - interests: number[]; - location: [number, number]; + location: { + lng: number; + lat: number; + }; + positionId: number; + techIds: number[]; + interestIds: number[]; email?: string; - linkedin?: string; instagram?: string; + linkedIn?: string; blog?: string; } - -export interface LoginResponse { - accessToken: string; - sessionId: string; -} - -export type FetchLoginResponse = LoginResponse; diff --git a/src/types/filter.types.ts b/src/types/filter.types.ts deleted file mode 100644 index aba34ce..0000000 --- a/src/types/filter.types.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface Position { - id: number; - name: string; - description: string; -} - -export interface PositionCategory { - category: string; - positions: readonly Position[]; -} - -export interface Stack { - id: number; - label: string; -} diff --git a/src/types/tags.types.ts b/src/types/tags.types.ts new file mode 100644 index 0000000..126d35c --- /dev/null +++ b/src/types/tags.types.ts @@ -0,0 +1,20 @@ +export interface Position { + id: number; + field: string; + role: string; +} + +export interface Interest { + id: number; + value: string; +} + +export interface Stack { + id: number; + value: string; +} + +export interface PositionsInterestsResponse { + positions: Position[]; + interests: Interest[]; +} From 0c11df7306a33292cfc7bedb9f1854d3f22c62ee Mon Sep 17 00:00:00 2001 From: Monixc Date: Fri, 8 Aug 2025 09:48:08 +0900 Subject: [PATCH 15/30] =?UTF-8?q?feat:=20API=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20=EC=9D=B8=EC=A6=9D,=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=EC=9E=90,=20=EA=B2=8C=EC=8B=9C=EB=AC=BC,=20=ED=83=9C?= =?UTF-8?q?=EA=B7=B8=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/api/auth.ts | 48 +++++++++++++++++++++++++++++++++++++++ src/lib/api/config.ts | 8 +++++++ src/lib/api/developers.ts | 15 ++++++++++++ src/lib/api/posts.ts | 13 +++++++++++ src/lib/api/tags.ts | 22 ++++++++++++++++++ 5 files changed, 106 insertions(+) create mode 100644 src/lib/api/auth.ts create mode 100644 src/lib/api/config.ts create mode 100644 src/lib/api/developers.ts create mode 100644 src/lib/api/posts.ts create mode 100644 src/lib/api/tags.ts diff --git a/src/lib/api/auth.ts b/src/lib/api/auth.ts new file mode 100644 index 0000000..153316c --- /dev/null +++ b/src/lib/api/auth.ts @@ -0,0 +1,48 @@ +import type { FetchLoginResponse, SignUpRequest } from "@/types/auth.types"; +import { createApiUrl, defaultHeaders } from "./config"; + +export const fetchLogin = async (code: string): Promise => { + const response = await fetch( + `${createApiUrl("/auth/sign-in")}?code=${encodeURIComponent(code)}`, + { + method: "GET", + headers: defaultHeaders, + } + ); + + if (!response.ok) { + throw new Error(`GitHub 로그인 실패: ${response.status}`); + } + + const data = await response.json(); + + if (response.status === 200) { + return { + accessToken: data.accessToken, + sessionId: data.sessionId, + result: data.result, + }; + } else { + throw new Error(`GitHub 로그인 실패: ${response.status}`); + } +}; + +export const fetchSignUp = async ( + sessionId: string, + data: SignUpRequest +): Promise => { + const url = `${createApiUrl("/auth/sign-up")}?sessionId=${encodeURIComponent( + sessionId + )}`; + + const response = await fetch(url, { + method: "POST", + headers: defaultHeaders, + body: JSON.stringify(data), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`회원가입 실패: ${response.status} - ${errorText}`); + } +}; diff --git a/src/lib/api/config.ts b/src/lib/api/config.ts new file mode 100644 index 0000000..cf48a1f --- /dev/null +++ b/src/lib/api/config.ts @@ -0,0 +1,8 @@ +export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ""; + +export const createApiUrl = (endpoint: string) => `${API_BASE_URL}${endpoint}`; + +export const defaultHeaders = { + "Content-Type": "application/json", + Accept: "application/json", +}; diff --git a/src/lib/api/developers.ts b/src/lib/api/developers.ts new file mode 100644 index 0000000..bc7a53e --- /dev/null +++ b/src/lib/api/developers.ts @@ -0,0 +1,15 @@ +import type { Developer } from "@/types/developer.types"; +import type { ExploreTabValue } from "@/constants/explore"; +import { createApiUrl } from "./config"; + +export const fetchDevelopers = async ( + category: ExploreTabValue +): Promise => { + const response = await fetch( + `${createApiUrl("/developers")}?category=${category}` + ); + if (!response.ok) { + throw new Error("개발자 목록을 불러오는 데 실패했습니다."); + } + return response.json(); +}; diff --git a/src/lib/api/posts.ts b/src/lib/api/posts.ts new file mode 100644 index 0000000..80c93f8 --- /dev/null +++ b/src/lib/api/posts.ts @@ -0,0 +1,13 @@ +import type { Post } from "@/types/board.types"; +import type { UICategory } from "@/constants/board"; +import { createApiUrl } from "./config"; + +export const fetchPosts = async (category: UICategory): Promise => { + const response = await fetch( + `${createApiUrl("/posts")}?category=${category}` + ); + if (!response.ok) { + throw new Error("게시물 목록을 불러오는 데 실패했습니다."); + } + return response.json(); +}; diff --git a/src/lib/api/tags.ts b/src/lib/api/tags.ts new file mode 100644 index 0000000..61be2c4 --- /dev/null +++ b/src/lib/api/tags.ts @@ -0,0 +1,22 @@ +import type { Stack, PositionsInterestsResponse } from "@/types/tags.types"; +import { API_BASE_URL } from "./config"; + +export const fetchStacks = async (keyword: string = ""): Promise => { + const response = await fetch( + `${API_BASE_URL}/tags/tech-stack?keyword=${encodeURIComponent(keyword)}` + ); + if (!response.ok) { + throw new Error("기술 스택 목록을 불러오는 데 실패했습니다."); + } + const data = await response.json(); + return data.results; +}; + +export const fetchPositionsInterests = + async (): Promise => { + const response = await fetch(`${API_BASE_URL}/tags/positions-interests`); + if (!response.ok) { + throw new Error("포지션과 관심사 목록을 불러오는 데 실패했습니다."); + } + return response.json(); + }; From dbc9955eece0c94cdd5a585568dd504e72d23b78 Mon Sep 17 00:00:00 2001 From: Monixc Date: Fri, 8 Aug 2025 09:49:05 +0900 Subject: [PATCH 16/30] =?UTF-8?q?refactor:=20API=20=ED=98=B8=EC=B6=9C=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/explore/index.tsx | 2 +- src/pages/home/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/explore/index.tsx b/src/pages/explore/index.tsx index 588e2c5..5093dc3 100644 --- a/src/pages/explore/index.tsx +++ b/src/pages/explore/index.tsx @@ -8,7 +8,7 @@ import { ListView } from "./_components/layout/ListView"; import { GridView } from "./_components/layout/GridView"; import { AppTabs } from "../../components/common/AppTabs"; import { cn } from "../../lib/utils"; -import { fetchDevelopers } from "../../lib/api"; +import { fetchDevelopers } from "../../lib/api/developers"; export const ExplorePage = () => { const [activeTab, setActiveTab] = useState("nearby"); diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 857b880..7244781 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { Bell, Plus, Search } from "lucide-react"; import { useParams, useNavigate } from "react-router-dom"; import type { Post } from "../../types/board.types"; -import { fetchPosts } from "../../lib/api"; +import { fetchPosts } from "../../lib/api/posts"; import { AppTabs } from "../../components/common/AppTabs"; import { AddPostModal } from "./_components/modal/AddPostModal"; import { ListItem } from "./_components/ListItem"; From 7db063bb267d2df4eb84aad4ec4207bf8dffd7fa Mon Sep 17 00:00:00 2001 From: Monixc Date: Fri, 8 Aug 2025 09:51:11 +0900 Subject: [PATCH 17/30] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EB=B0=8F=20?= =?UTF-8?q?=ED=83=9C=EA=B7=B8=20=EA=B4=80=EB=A0=A8=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=ED=9B=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/queries/useAuth.ts | 22 ++++++++++++++++++++++ src/hooks/queries/useTags.ts | 17 +++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/hooks/queries/useAuth.ts create mode 100644 src/hooks/queries/useTags.ts diff --git a/src/hooks/queries/useAuth.ts b/src/hooks/queries/useAuth.ts new file mode 100644 index 0000000..6eb28bf --- /dev/null +++ b/src/hooks/queries/useAuth.ts @@ -0,0 +1,22 @@ +import { useMutation } from "@tanstack/react-query"; +import { fetchLogin, fetchSignUp } from "@/lib/api/auth"; +import type { FetchLoginResponse, SignUpRequest } from "@/types/auth.types"; + +export const useLogin = () => { + return useMutation({ + mutationFn: async (code: string) => { + const result = await fetchLogin(code); + return result; + }, + retry: false, + }); +}; + +export const useSignUp = () => { + return useMutation({ + mutationFn: async ({ sessionId, data }) => { + await fetchSignUp(sessionId, data); + }, + retry: false, + }); +}; diff --git a/src/hooks/queries/useTags.ts b/src/hooks/queries/useTags.ts new file mode 100644 index 0000000..2197a9c --- /dev/null +++ b/src/hooks/queries/useTags.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { fetchStacks, fetchPositionsInterests } from "@/lib/api/tags"; +import type { Stack, PositionsInterestsResponse } from "@/types/tags.types"; + +export const useStacks = (keyword: string = "") => { + return useQuery({ + queryKey: ["stacks", keyword], + queryFn: () => fetchStacks(keyword), + }); +}; + +export const usePositionsInterests = () => { + return useQuery({ + queryKey: ["positions-interests"], + queryFn: fetchPositionsInterests, + }); +}; From 41e0f73b7e4d5cb1a9f6ce63a2d2cb37823cb999 Mon Sep 17 00:00:00 2001 From: Monixc Date: Fri, 8 Aug 2025 09:54:32 +0900 Subject: [PATCH 18/30] =?UTF-8?q?feat:=20=ED=8F=AC=EC=A7=80=EC=85=98=20?= =?UTF-8?q?=EB=B0=8F=20=EC=8A=A4=ED=83=9D=20=EC=84=A0=ED=83=9D=EA=B8=B0?= =?UTF-8?q?=EC=97=90=EC=84=9C=20API=20=ED=98=B8=EC=B6=9C=EC=9D=84=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/PositionSelector.tsx | 59 ++++++++++------------ src/components/common/StackSelector.tsx | 34 ++++--------- 2 files changed, 38 insertions(+), 55 deletions(-) diff --git a/src/components/common/PositionSelector.tsx b/src/components/common/PositionSelector.tsx index d9d7e0f..8e04ad8 100644 --- a/src/components/common/PositionSelector.tsx +++ b/src/components/common/PositionSelector.tsx @@ -1,4 +1,3 @@ -import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Accordion, @@ -8,8 +7,8 @@ import { } from "@/components/ui/accordion"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; -import { fetchPositions } from "@/lib/api"; -import type { PositionCategory } from "@/types/filter.types"; +import { usePositionsInterests } from "@/hooks/queries/useTags"; +import type { Position } from "@/types/tags.types"; interface PositionSelectorProps { selectedPosition: number | null; @@ -22,28 +21,27 @@ export const PositionSelector = ({ onPositionChange, label = "포지션", }: PositionSelectorProps) => { - const [positions, setPositions] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const loadPositions = async () => { - try { - const fetchedPositions = await fetchPositions(); - setPositions(fetchedPositions); - } catch (error) { - console.error("포지션 로딩 실패:", error); - } finally { - setIsLoading(false); - } - }; - - loadPositions(); - }, []); + const { data: positionsInterestsData, isLoading } = usePositionsInterests(); if (isLoading) { return
로딩 중...
; } + if (!positionsInterestsData?.positions) { + return
포지션을 불러올 수 없습니다.
; + } + + const positions = positionsInterestsData.positions; + + // positions를 field별로 그룹화 + const groupedPositions = positions.reduce((acc, position) => { + if (!acc[position.field]) { + acc[position.field] = []; + } + acc[position.field].push(position); + return acc; + }, {} as Record); + return (
@@ -51,31 +49,28 @@ export const PositionSelector = ({ type="single" collapsible className="w-full border rounded-md border-brand-surface"> - {positions.map((cat) => ( + {Object.entries(groupedPositions).map(([field, fieldPositions]) => ( - {cat.category} + {field}
- {cat.positions.map((pos) => ( + {fieldPositions.map((position) => ( ))} diff --git a/src/components/common/StackSelector.tsx b/src/components/common/StackSelector.tsx index fdcb202..622d3a8 100644 --- a/src/components/common/StackSelector.tsx +++ b/src/components/common/StackSelector.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Check, ChevronsUpDown, X } from "lucide-react"; @@ -16,8 +16,8 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { Label } from "@/components/ui/label"; -import { fetchStacks } from "@/lib/api"; -import type { Stack } from "@/types/filter.types"; +import { useStacks } from "@/hooks/queries/useTags"; +import type { Stack } from "@/types/tags.types"; interface StackSelectorProps { selectedStackIds: number[]; @@ -32,24 +32,8 @@ export const StackSelector = ({ label = "기술 스택 / 분야", maxSelection, }: StackSelectorProps) => { - const [stacks, setStacks] = useState([]); const [open, setOpen] = useState(false); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const loadStacks = async () => { - try { - const fetchedStacks = await fetchStacks(); - setStacks(fetchedStacks); - } catch (error) { - console.error("스택 로딩 실패:", error); - } finally { - setIsLoading(false); - } - }; - - loadStacks(); - }, []); + const { data: stacks, isLoading } = useStacks(); const handleSelectStack = (stackId: number) => { const newStackIds = selectedStackIds.includes(stackId) @@ -71,6 +55,10 @@ export const StackSelector = ({ return
로딩 중...
; } + if (!stacks) { + return
기술 스택을 불러올 수 없습니다.
; + } + return (
@@ -102,7 +90,7 @@ export const StackSelector = ({ {stacks.map((stack) => ( handleSelectStack(stack.id)} className="aria-selected:bg-brand-primary text-white"> - {stack.label} + {stack.value} ))} @@ -129,7 +117,7 @@ export const StackSelector = ({ key={stackId} variant="secondary" className="flex items-center gap-x-1 bg-brand-primary text-black"> - {stack?.label} + {stack?.value} ))}
From 0a837939e13d2c10b83018a3929c3b7e3563bce3 Mon Sep 17 00:00:00 2001 From: Monixc Date: Fri, 8 Aug 2025 10:01:21 +0900 Subject: [PATCH 21/30] =?UTF-8?q?feat:=20GitHub=20=EC=BD=9C=EB=B0=B1?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8/=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20=EB=B6=84=EA=B8=B0=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EB=B3=80=EA=B2=BD,=20=EC=84=B8=EC=85=98=20?= =?UTF-8?q?=EC=9D=B4=EC=9D=B4=EB=94=94=EB=A5=BC=20=ED=8C=8C=EB=9E=8C?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=84=98=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useGitHubCallback.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/hooks/useGitHubCallback.ts b/src/hooks/useGitHubCallback.ts index 9746929..6475d26 100644 --- a/src/hooks/useGitHubCallback.ts +++ b/src/hooks/useGitHubCallback.ts @@ -1,7 +1,7 @@ import { useEffect, useRef } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { useMutation } from "@tanstack/react-query"; -import { fetchLogin } from "@/lib/api"; +import { fetchLogin } from "@/lib/api/auth"; export const useGitHubCallback = () => { const [searchParams] = useSearchParams(); @@ -15,11 +15,21 @@ export const useGitHubCallback = () => { sessionStorage.removeItem("github_oauth_state"); if (result.sessionId && !result.accessToken) { - navigate("/onboarding", { replace: true }); - } else if (result.accessToken) { - localStorage.setItem("access_token", result.accessToken); - localStorage.setItem("session_id", result.sessionId); + navigate( + `/onboarding?sessionId=${encodeURIComponent(result.sessionId)}`, + { replace: true } + ); + } else if (result.accessToken || result.result) { + // accessToken이 있거나 result가 있으면 (온보딩 완료된 사용자) + if (result.accessToken) { + localStorage.setItem("access_token", result.accessToken); + } + if (result.sessionId) { + localStorage.setItem("session_id", result.sessionId); + } navigate("/board", { replace: true }); + } else { + console.log(" 예상치 못한 응답:", result); } }, onError: (error) => { From bdc72f7d747750aad9c7f28e58ba0822eccd8e9d Mon Sep 17 00:00:00 2001 From: Monixc Date: Fri, 8 Aug 2025 10:24:34 +0900 Subject: [PATCH 22/30] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=204?= =?UTF-8?q?=EB=8B=A8=EA=B3=84=EC=97=90=EC=84=9C=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=A0=84=EB=8B=AC=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/OnboardingStep4.tsx | 76 +++++++++++++++++-- src/pages/onboarding/index.tsx | 2 +- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/pages/onboarding/_components/OnboardingStep4.tsx b/src/pages/onboarding/_components/OnboardingStep4.tsx index 6c5f4cd..cc0805d 100644 --- a/src/pages/onboarding/_components/OnboardingStep4.tsx +++ b/src/pages/onboarding/_components/OnboardingStep4.tsx @@ -1,14 +1,23 @@ import { useState, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; +import { useSignUp } from "@/hooks/queries/useAuth"; +import type { OnboardingData } from "../index"; -export function OnboardingStep4() { +interface StepProps { + data: OnboardingData; +} + +export function OnboardingStep4({ data }: StepProps) { const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const [showText1, setShowText1] = useState(false); const [showText2, setShowText2] = useState(false); const [showButton, setShowButton] = useState(false); + const signUpMutation = useSignUp(); + useEffect(() => { const timer1 = setTimeout(() => { setShowText1(true); @@ -29,8 +38,64 @@ export function OnboardingStep4() { }; }, []); - const handleSubmit = () => { - navigate("/board"); + const handleSubmit = async () => { + try { + const sessionId = searchParams.get("sessionId"); + + if (!sessionId) { + navigate("/login"); + return; + } + + if ( + !data.nickname || + !data.age || + !data.experience || + !data.bio || + !data.location || + !data.position || + !data.techStack || + !data.interests + ) { + console.error("온보딩 데이터가 불완전합니다:", { + nickname: data.nickname, + age: data.age, + experience: data.experience, + bio: data.bio, + location: data.location, + position: data.position, + techStack: data.techStack, + interests: data.interests, + }); + return; + } + + const requestData = { + nickname: data.nickname, + age: data.age, + experience: data.experience, + bio: data.bio, + location: { + lng: data.location.lng, + lat: data.location.lat, + }, + positionId: data.position, + techIds: data.techStack, + interestIds: data.interests, + ...(data.email && { email: data.email }), + ...(data.instagram && { instagram: data.instagram }), + ...(data.linkedin && { linkedIn: data.linkedin }), + ...(data.blog && { blog: data.blog }), + }; + + await signUpMutation.mutateAsync({ + sessionId, + data: requestData, + }); + navigate("/board"); + } catch (error) { + alert("회원가입에 실패했습니다. 다시 시도해주세요."); + } }; return ( @@ -58,8 +123,9 @@ export function OnboardingStep4() { )}>
diff --git a/src/pages/onboarding/index.tsx b/src/pages/onboarding/index.tsx index bb24be0..6f21586 100644 --- a/src/pages/onboarding/index.tsx +++ b/src/pages/onboarding/index.tsx @@ -209,7 +209,7 @@ export function OnboardingPage() { data={context} /> )} - Complete={() => } + Complete={({ context }) => } /> From e07aea2d8576ca095d788f6b3c63985b21f14fe1 Mon Sep 17 00:00:00 2001 From: Monixc Date: Fri, 8 Aug 2025 14:00:32 +0900 Subject: [PATCH 23/30] =?UTF-8?q?feat:=20/board=EB=A1=9C=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91=ED=95=98=EB=8A=94=20=EB=AA=A8=EB=93=A0=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=EC=97=90=EC=84=9C=20home=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20active?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layout/BottomNavigation.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/layout/BottomNavigation.tsx b/src/components/layout/BottomNavigation.tsx index f6f64e5..68e15ae 100644 --- a/src/components/layout/BottomNavigation.tsx +++ b/src/components/layout/BottomNavigation.tsx @@ -10,7 +10,10 @@ export default function BottomNavigation() {
@@ -79,22 +114,40 @@ export const AddPostModal = ({ - {BOARD_CATEGORIES.filter((cat) => cat.value !== "all").map( - (category) => ( - - {category.label} - - ) - )} + {boards.map((board) => ( + + {board.value} + + ))}
{/*포스트, 리크루트 조건부 렌더링*/} - {isRecruitBoard ? : } + {isRecruitBoard ? ( + + ) : ( + + )}
); diff --git a/src/pages/home/_components/modal/PostItem.tsx b/src/pages/home/_components/modal/PostItem.tsx index 554737e..fdd3128 100644 --- a/src/pages/home/_components/modal/PostItem.tsx +++ b/src/pages/home/_components/modal/PostItem.tsx @@ -1,19 +1,27 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; -import { useAddPostModal } from "../../../../hooks/useAddPostModal"; -export const PostItem = () => { - const { title, content, setTitle, setContent } = useAddPostModal(); +interface PostItemProps { + title: string; + content: string; + onTitleChange: (title: string) => void; + onContentChange: (content: string) => void; +} +export const PostItem = ({ + title, + content, + onTitleChange, + onContentChange, +}: PostItemProps) => { return (
- {/* 제목 입력 */}
setTitle(e.target.value)} + onChange={(e) => onTitleChange(e.target.value)} placeholder="제목을 입력하세요" className="bg-brand-surface border-transparent focus:!ring-0 focus:!border-brand-primary" maxLength={100} @@ -22,13 +30,11 @@ export const PostItem = () => { {title.length}/100
- - {/* 내용 입력 */}