Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.env
.env
*storybook.log
18 changes: 18 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
"stories": [
"../src/**/*.mdx",
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
],
"addons": [
"@storybook/addon-essentials",
"@storybook/addon-onboarding",
"@chromatic-com/storybook"
],
"framework": {
"name": "@storybook/react-vite",
"options": {}
}
};
export default config;
14 changes: 14 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Preview } from "@storybook/react";

const preview: Preview = {
parameters: {
controls: {
matchers: {
// color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};

export default preview;
8,689 changes: 5,472 additions & 3,217 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest"
"test": "vitest",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build -o storybook-static"
},
"dependencies": {
"@emotion/react": "^11.13.3",
Expand All @@ -17,6 +19,7 @@
"@mui/styled-engine-sc": "^6.1.1",
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-toast": "^1.2.1",
"@tanstack/react-query": "^5.59.9",
"axios": "^1.7.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
10 changes: 7 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { MyPage } from "./pages/my-page";
import { MyPageEdit } from "./pages/my-page-edit";
import { Toaster } from "sonner";
import { BuddyPage } from "./pages/buddy-page";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const router = createBrowserRouter([
{
Expand Down Expand Up @@ -36,13 +37,16 @@ const router = createBrowserRouter([
},
]);

// QueryClient 인스턴스 생성
const queryClient = new QueryClient();

export const App = () => {
// useAuthCheck();
return (
<>
{/* <Header /> */}
<QueryClientProvider client={queryClient}>
<Toaster position="bottom-center" richColors />
<GlobalStyles />
<RouterProvider router={router}></RouterProvider>
</>
</QueryClientProvider>
);
};
11 changes: 7 additions & 4 deletions src/components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import styled from "styled-components";
import { useNavigate } from "react-router-dom";
import { COLORS } from "../theme";

import logo from "../assets/images/peerLogo.png";
import userIcon from "../assets/images/user.png";
// import { useLocation } from "react-router-dom";
import { useUserAuth } from "../services/queries/user.queries";

export const Header = () => {
const token = localStorage.getItem("accessToken"); // localStorage에서 토큰 가져오기
const navigate = useNavigate();
// const location = useLocation();

// console.log("location:", location);
// React Query 캐시에서 로그인 정보 및 토큰 가져오기
const { data: authData } = useUserAuth();
const token = authData?.accessToken; // 캐시된 토큰
console.log(token);

const handleLoginClick = () => {
if (token) {
navigate("/my-page"); // 유저 아이콘 클릭 시 /my-page로 이동
Expand All @@ -34,6 +36,7 @@ export const Header = () => {
);
};

// 스타일 컴포넌트는 동일
const Container = styled.div`
width: 100%;
height: 48px;
Expand Down
1 change: 1 addition & 0 deletions src/components/register/registerStep4.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const RegisterStep4 = () => {
const major = localStorage.getItem("major");
const name = localStorage.getItem("name");

console.log(major);
// 닉네임 중복 확인
const handleCheckNickname = async () => {
if (nickname) {
Expand Down
56 changes: 10 additions & 46 deletions src/pages/login-page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// src/pages/LoginPage.tsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import styled from "styled-components";
import { SigninType, signInSchema } from "../lib/schema/auth.schema"; // 스키마 및 타입 임포트
import { toast } from "sonner";
import { useNavigate } from "react-router-dom";
import { COLORS } from "../theme";
import { LoginHeader } from "../components/loginHeader";
import { axiosInstance } from "../services/axios-instance"; // axiosInstance 가져오기
import { useSignin } from "../services/queries/user.mutation";

export const LoginPage = () => {
const navigate = useNavigate();
Expand All @@ -19,50 +19,11 @@ export const LoginPage = () => {
resolver: zodResolver(signInSchema), // Zod 스키마를 리졸버로 사용
});

// 로그인 요청 처리
// 로그인 mutation 훅 사용
const { mutateAsync: login, isPending: isSigninLoading } = useSignin();

const onSubmit = async (data: SigninType) => {
try {
const response = await axiosInstance.post("/auth/sign-in", {
account: data.username, // API에서 요구하는 필드명
password: data.password,
});

console.log(response.data);
// 서버로부터 받은 토큰을 로컬 스토리지에 저장
const {
accessToken,
account,
gender,
grade,
kakaoAccount,
major,
minor,
name,
nickname,
phoneNumber,
refreshToken,
studentId,
} = response.data.data;
// 데이터를 로컬 스토리지에 저장
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
localStorage.setItem("name", name); // 유저 이름도 저장
localStorage.setItem("account", account);
localStorage.setItem("gender", gender);
localStorage.setItem("grade", grade.toString()); // 숫자일 경우 문자열로 변환
localStorage.setItem("kakaoAccount", kakaoAccount);
localStorage.setItem("major", major);
localStorage.setItem("minor", minor);
localStorage.setItem("nickname", nickname);
localStorage.setItem("phoneNumber", phoneNumber);
localStorage.setItem("studentId", studentId);

toast.success("로그인 성공!");
navigate("/"); // 로그인 성공 시 메인 페이지로 이동
} catch (error) {
console.error("로그인 중 오류 발생:", error);
toast.error("로그인 실패! 아이디 또는 비밀번호를 확인해 주세요.");
}
await login(data); // 로그인 요청
};

return (
Expand Down Expand Up @@ -93,7 +54,9 @@ export const LoginPage = () => {
<ErrorMessage>{errors.password.message}</ErrorMessage>
)}
</FormItem>
<SubmitButton type="submit">로그인</SubmitButton>
<SubmitButton type="submit" disabled={isSigninLoading}>
{isSigninLoading ? "로그인 중..." : "로그인"}
</SubmitButton>
</Form>
<Container2>
<Find>ID • PW 찾기</Find>
Expand All @@ -108,6 +71,7 @@ export const LoginPage = () => {
);
};

// 스타일 컴포넌트는 동일
const Container = styled.div`
display: flex;
flex-direction: column;
Expand Down
35 changes: 35 additions & 0 deletions src/services/apis/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,38 @@ export const registerUser = async (formData: object) => {
return null;
}
};

// 회원탈퇴 API 요청 함수
export const deleteUserAccount = async () => {
try {
const accessToken = localStorage.getItem("accessToken");
const refreshToken = localStorage.getItem("refreshToken");

console.log(accessToken);
console.log(refreshToken);

if (!accessToken || !refreshToken) {
toast.error("로그인이 필요합니다. 다시 로그인 해주세요.");
return null;
}

const response = await axiosInstance.delete("/member/my-page", {
headers: {
Authorization: `Bearer ${accessToken}`,
"x-refresh-token": refreshToken,
},
});

if (response.status === 200) {
toast.success("회원 탈퇴가 성공적으로 완료되었습니다.");
return response.data;
} else {
toast.error("회원 탈퇴에 실패했습니다.");
return null;
}
} catch (error) {
console.error("회원 탈퇴 중 오류 발생: ", error);
toast.error("회원 탈퇴 중 오류가 발생했습니다. 다시 시도해주세요.");
return null;
}
};
1 change: 0 additions & 1 deletion src/services/axios-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ axiosInstance.interceptors.request.use(
(config) => {
const accessToken = window.localStorage.getItem("accessToken");
const refreshToken = window.localStorage.getItem("refreshToken");

if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
Expand Down
49 changes: 49 additions & 0 deletions src/services/queries/user.mutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { axiosInstance } from "../../services/axios-instance";
import { SigninType } from "../../lib/schema/auth.schema";
import { toast } from "sonner";
import { useNavigate } from "react-router-dom";

const signin = async (data: SigninType) => {
const response = await axiosInstance.post("/auth/sign-in", {
account: data.username,
password: data.password,
});
return response.data;
};

// 로그인 mutation 훅
export const useSignin = () => {
const queryClient = useQueryClient();
const navigate = useNavigate();

return useMutation({
mutationFn: signin,
onError: (error) => {
console.error("로그인 중 오류 발생:", error);
toast.error("로그인 실패! 아이디 또는 비밀번호를 확인해 주세요.");
},
onSuccess: (data) => {
const {
accessToken,
refreshToken,
...userInfo // 나머지 유저 정보
} = data.data;

// localStorage에 토큰 저장
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
console.log(data.data);

// React Query 캐시에 토큰 및 사용자 정보 저장
queryClient.setQueryData(["user", "auth"], {
accessToken,
refreshToken,
userInfo,
});

toast.success("로그인 성공!");
navigate("/"); // 로그인 성공 시 메인 페이지로 이동
},
});
};
15 changes: 15 additions & 0 deletions src/services/queries/user.queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useQuery } from "@tanstack/react-query";
import { AuthData } from "../../types/user";

// 캐시된 사용자 정보 및 토큰 가져오기 훅
export const useUserAuth = () => {
return useQuery<AuthData | null>({
queryKey: ["user", "auth"],
queryFn: () => {
// 여기는 실제 데이터를 가져오지 않으므로 비어 있을 수 있음
return null;
},
enabled: false, // 이미 캐시에 있는 데이터를 가져오므로 API 호출을 하지 않음
select: (data) => data, // 캐시된 전체 데이터를 선택하여 반환
});
};
53 changes: 53 additions & 0 deletions src/stories/Button.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';

import { Button } from './Button';

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
title: 'Example/Button',
component: Button,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {
backgroundColor: { control: 'color' },
},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
args: { onClick: fn() },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Primary: Story = {
args: {
primary: true,
label: 'Button',
},
};

export const Secondary: Story = {
args: {
label: 'Button',
},
};

export const Large: Story = {
args: {
size: 'large',
label: 'Button',
},
};

export const Small: Story = {
args: {
size: 'small',
label: 'Button',
},
};
Loading