Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
843a062
design: 모바일 뷰 기본 틀 설정 (#2)
AAminha Feb 17, 2025
9f6bb67
design: 마이페이지 퍼블리싱 (#8)
AAminha Feb 18, 2025
1d6852d
design: 알림페이지 퍼블리싱 (#10)
AAminha Feb 18, 2025
8c3c23c
design: 편지작성 페이지 퍼블리싱 (#13)
wldnjs990 Feb 19, 2025
401c902
design: 게시판 퍼블리싱 (#14)
AAminha Feb 20, 2025
0fa1e07
design: 내 편지함 페이지 퍼블리싱 (#16)
AAminha Feb 20, 2025
a8e7622
design : 랜덤 편지 페이지 & 편지 상세 페이지 퍼블리싱 (#17)
wldnjs990 Feb 20, 2025
59d89d4
design: 온보딩 페이지 퍼블리싱 (#18)
nirii00 Feb 20, 2025
ba49647
design: 홈 페이지 퍼블리싱 (#19)
tifsy Feb 21, 2025
4c90dbc
design: 로그인 및 랜딩 페이지 퍼블리싱 (#22)
AAminha Feb 21, 2025
5b79a33
design: 홈 페이지 공유 미리보기 퍼블리싱 (#25)
tifsy Feb 21, 2025
73835b9
design, feat, chore : 편지 작성 페이지 추가 퍼블리싱 + 기능구현 + API연결 + 관리자페이지 퍼블리싱 …
wldnjs990 Feb 24, 2025
2bea6b0
feat: 내 편지함 기능 구현 1차 (#30)
nirii00 Feb 24, 2025
70ae3a3
design: 404 페이지 디자인 (#33)
nirii00 Feb 24, 2025
645c527
feat: 롤링페이퍼 기능 구현 (mock api) (#35)
AAminha Feb 25, 2025
0b47e49
feat: 오고 있는 편지 조회 기능 구현 (mock api) (#36)
tifsy Feb 25, 2025
f8358f8
feat: 마이페이지 내 정보 조회 기능 구현 (mock api) (#39)
tifsy Feb 25, 2025
699fa09
fix: 온보딩 애니메이션 오류 해결 (#41)
nirii00 Feb 26, 2025
5ca4f51
feat:관리자 페이지 1차 기능 구현 (#43)
wldnjs990 Feb 26, 2025
b8b7bef
feat:편지 작성 페이지 1차 기능구현 (#47)
wldnjs990 Feb 27, 2025
2efb0d4
feat:랜덤 편지 + 편지 상세 1차 기능 구현 (#46)
wldnjs990 Feb 27, 2025
285cdae
Perf: 내 편지함 탄스택 쿼리 적용 (#50)
nirii00 Feb 28, 2025
7662866
feat: 편지 게시글 공유 기능 구현 (mock api) (#53)
tifsy Mar 2, 2025
bf32263
feat: 로그인 기능구현 (#52)
nirii00 Mar 3, 2025
fc4c223
FIx, Feat: 로그인 기능 버그 수정, 권한별 접근 페이지 기능 추가 (#56)
nirii00 Mar 4, 2025
911533d
feat: 읽지 않은 편지 수 조회 기능 구현 (#66)
tifsy Mar 4, 2025
3071137
feat : 편지작성, 랜덤편지 구현 90% 완료, 편지상세는 이후에 테스트 (#67)
wldnjs990 Mar 4, 2025
01e9bbb
refactor: PR 리뷰를 반영한 리팩토링 (#70)
tifsy Mar 4, 2025
1742006
fix: 게시판 편지 공유 모달 오류 해결 (#73)
tifsy Mar 5, 2025
2cce4ee
fix: 편지함 오류 수정 (#72)
nirii00 Mar 5, 2025
9812d33
feat: 임시저장된 편지 조회 기능 구현 (#75)
tifsy Mar 5, 2025
cbe92b0
feat: 게시판 1차 기능구현 (#76)
nirii00 Mar 6, 2025
177f8cf
feat : 관리자 & 신고 2차 구현 (#77)
wldnjs990 Mar 6, 2025
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
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --host 0.0.0.0",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@egjs/react-infinitegrid": "^4.12.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.4.4",
"@mui/material": "^6.4.4",
"@tailwindcss/vite": "^4.0.6",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.7.9",
"gsap": "^3.12.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-intersection-observer": "^9.15.1",
"react-router": "^7.1.5",
"swiper": "^11.2.4",
"tailwind-merge": "^3.0.1",
"tailwindcss": "^4.0.6",
"zustand": "^5.0.3"
Expand Down
1,610 changes: 1,606 additions & 4 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

65 changes: 50 additions & 15 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,74 @@
import { Route, Routes } from 'react-router';

import useViewport from './hooks/useViewport';
import Layout from './layouts/Layout';
import MobileLayout from './layouts/MobileLayout';
import PrivateRoute from './layouts/PrivateRoute';
import AdminPage from './pages/Admin';
import FilteredLetterManage from './pages/Admin/FilteredLetter';
import FilteringManage from './pages/Admin/Filtering';
import ReportManage from './pages/Admin/Report';
import AuthCallbackPage from './pages/Auth';
import Home from './pages/Home';
import Landing from './pages/Landing';
import LetterBoardPage from './pages/LetterBoard';
import LetterBoardDetailPage from './pages/LetterBoardDetail';
import LetterBoxPage from './pages/LetterBox';
import LetterBoxDetailPage from './pages/LetterBoxDetail';
import LetterDetailPage from './pages/LetterDetail';
import LoginPage from './pages/Login';
import MyPage from './pages/MyPage';
import MyBoardPage from './pages/MyPage/components/MyBoardPage';
import NotFoundPage from './pages/NotFound';
import NotificationsPage from './pages/Notifications';
import OnboardingPage from './pages/Onboarding';
import RandomLettersPage from './pages/RandomLetters';
import RollingPaperPage from './pages/RollingPaper';
import WritePage from './pages/Write';

const App = () => {
useViewport();

return (
<Routes>
<Route>
<Route index element={<Home />} />
<Route element={<MobileLayout />}>
<Route path="login" element={<LoginPage />} />
<Route path="landing" element={<Landing />} />
<Route path="*" element={<NotFoundPage />} />
<Route path="auth-callback" element={<AuthCallbackPage />} />
<Route index element={<Home />} />
<Route path="onboarding" element={<OnboardingPage />} />
<Route path="letter">
<Route path="random" element={<RandomLettersPage />} />
<Route path="box" element={<LetterBoxPage />} />
<Route path="write" element={<WritePage />} />
<Route path=":id" element={<LetterDetailPage />} />
</Route>
<Route path="board">
<Route path="letter" element={<LetterBoardPage />} />
<Route path="letter/:id" element={<LetterBoardDetailPage />} />
<Route path="rolling/:id" element={<RollingPaperPage />} />

<Route element={<PrivateRoute />}>
<Route path="letter">
<Route element={<Layout />}>
<Route path="random" element={<RandomLettersPage />} />
<Route path="box" element={<LetterBoxPage />} />
<Route path="box/:id" element={<LetterBoxDetailPage />} />
</Route>
<Route path="write" element={<WritePage />} />
<Route path=":id" element={<LetterDetailPage />} />
</Route>
<Route path="board">
<Route element={<Layout />}>
<Route path="rolling/:id" element={<RollingPaperPage />} />
<Route path="letter" element={<LetterBoardPage />} />
</Route>
<Route path="letter/:id" element={<LetterBoardDetailPage />} />
</Route>
<Route path="mypage" element={<Layout />}>
<Route index element={<MyPage />} />
<Route path="board" element={<MyBoardPage />} />
<Route path="notifications" element={<NotificationsPage />} />
</Route>
</Route>
<Route path="mypage">
<Route index element={<MyPage />} />
<Route path="notifications" element={<NotificationsPage />} />
</Route>

<Route element={<PrivateRoute />}>
<Route path="admin" element={<AdminPage />}>
<Route path="report" element={<ReportManage />} />
<Route path="badwords" element={<FilteringManage />} />
<Route path="filtered-letter" element={<FilteredLetterManage />} />
</Route>
</Route>
</Routes>
Expand Down
79 changes: 79 additions & 0 deletions src/apis/admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import client from './client';

const postReports = async (postReportRequest: PostReportRequest) => {
try {
const res = await client.post(`/api/reports`, postReportRequest);
if (res.status === 200) {
return res;
}
} catch (error) {
console.error(error);
}
};

const getReports = async (reportQueryString: ReportQueryString) => {
try {
const queryParams = new URLSearchParams();
if (reportQueryString.reportType !== null)
queryParams.append('reportType', reportQueryString.reportType);
if (reportQueryString.status !== null) queryParams.append('status', reportQueryString.status);
if (reportQueryString.page !== null) queryParams.append('page', reportQueryString.page);
if (reportQueryString.size !== null) queryParams.append('size', reportQueryString.size);

const queryStrings = queryParams.toString();
const res = await client.get(`/api/reports?${queryStrings}`);
if (!res) throw new Error('신고 목록 데이터 조회 도중 에러가 발생했습니다.');
console.log(res);
return res;
} catch (error) {
console.error(error);
}
};

const patchReport = async (reportId: number, patchReportRequest: PatchReportRequest) => {
try {
console.log(`/api/reports/${reportId}`, patchReportRequest);
const res = await client.patch(`/api/reports/${reportId}`, patchReportRequest);
console.log(res);
} catch (error) {
console.error(error);
}
};

// badwords
const getBadWords = async (setBadWords: React.Dispatch<React.SetStateAction<BadWords[]>>) => {
try {
const res = await client.get('/api/bad-words');
setBadWords(res.data.data);
console.log(res);
} catch (error) {
console.error(error);
}
};

const postBadWords = async (badWordsRequest: BadWords, callBack?: () => void) => {
try {
const res = await client.post('/api/bad-words', badWordsRequest);
if (callBack) callBack();
console.log(res);
} catch (error) {
console.error(error);
}
};

// 내 상상대로 만든 필터링 단어 취소 버튼
const patchBadWords = async (
badWordId: number,
badWordsRequest: BadWords,
callBack?: () => void,
) => {
try {
const res = await client.patch(`/api/bad-words/${badWordId}/status`, badWordsRequest);
if (callBack) callBack();
console.log(res);
} catch (error) {
console.error(error);
}
};

export { postReports, getReports, patchReport, getBadWords, postBadWords, patchBadWords };
77 changes: 77 additions & 0 deletions src/apis/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import client from './client';

export const socialLogin = (loginType: LoginType) => {
window.location.href = `${import.meta.env.VITE_API_URL}/oauth2/authorization/${loginType}`;
};

export const getUserToken = async (stateToken: string) => {
try {
const response = await client.get(`/api/auth/token?state=${stateToken}`);
if (!response) throw new Error('getUserToken: Error while fetching user token');
const userInfo = response.data;
if (userInfo) {
return userInfo;
}
return response;
} catch (error) {
console.error(error);
throw error;
}
};

export const postZipCode = async () => {
try {
const response = await client.post(`/api/members/zipCode`);
if (!response) throw new Error('fail to post ZipCode');
return response;
} catch (error) {
console.error(error);
}
};

export const getNewToken = async () => {
try {
const response = await client.post('/api/reissue', {}, { withCredentials: true });
if (!response) throw new Error('getNewToken: no response data');
return response;
} catch (error) {
console.error(error);
}
};

export const getMydata = async () => {
try {
const response = await client.get('/api/members/me');
if (!response) throw new Error('getNewToken: no response data');
return response;
} catch (error) {
console.error(error);
}
};

export const deleteUserInfo = async () => {
try {
const response = await client.delete('/api/members/me', {
withCredentials: true,
});
if (!response) throw new Error('deleteUserInfo: no response');
return response;
} catch (error) {
console.error(error);
}
};

export const postLogout = async () => {
try {
console.log(' before logout');

const response = await client.post('/api/logout', { withCredentials: true });
console.log('logout', response);
if (!response) throw new Error('postLogout: failed to logout');
return response;
} catch (error) {
console.log('logout error');

console.error(error);
}
};
110 changes: 109 additions & 1 deletion src/apis/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,113 @@
import axios from 'axios';

export const client = axios.create({
import useAuthStore from '@/stores/authStore';

import { getNewToken } from './auth';

const client = axios.create({
baseURL: import.meta.env.VITE_API_URL,
headers: { 'Content-Type': 'application/json' },
});

// type FailedRequest = {
// resolve: (token: string) => void;
// reject: (error: unknown) => void;
// };

let isRefreshing = false;
// let failedQueue: FailedRequest[] = [];

// const processQueue = (error: unknown, token: string | null = null) => {
// failedQueue.forEach((prom) => {
// if (error) {
// prom.reject(error);
// } else {
// if (token) {
// prom.resolve(token);
// }
// }
// });

// failedQueue = [];
// };

const callReissue = async () => {
try {
const response = await getNewToken();
const newToken = response?.data.accessToken;
return newToken;
} catch (e) {
return Promise.reject(e);
}
};

let retry = false;

client.interceptors.request.use(
(config) => {
console.log('response again', config);

const accessToken = useAuthStore.getState().accessToken;
if (config.url !== '/auth/reissue' && accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => Promise.reject(error),
);

client.interceptors.response.use(
(response) => response,
async (error) => {
const setAccessToken = useAuthStore.getState().setAccessToken;
const logout = useAuthStore.getState().logout;
const isLoggedIn = useAuthStore.getState().isLoggedIn;

const originalRequest = error.config;

if (!originalRequest || originalRequest.url === '/auth/reissue') {
if (isLoggedIn) logout();
return Promise.reject(error);
}

if ((error.response?.status === 401 || error.response?.status === 403) && !retry) {
retry = true;
if (isRefreshing) {
if (isLoggedIn) logout();
// try {
// return new Promise((resolve, reject) => {
// failedQueue.push({
// resolve: (token: string) => {
// originalRequest.headers.Authorization = `Bearer ${token}`;
// resolve(client(originalRequest));
// },
// reject: (err: unknown) => reject(err),
// });
// });
// } catch (e) {
// return Promise.reject(e);
// }
} else {
isRefreshing = true;
try {
const newToken = await callReissue();
setAccessToken(newToken);
// processQueue(null, newToken);
isRefreshing = false;
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return client(originalRequest);
} catch (e) {
// processQueue(e, null);
isRefreshing = false;
if (isLoggedIn) logout();
return Promise.reject(e);
}
}
}
if (isLoggedIn) logout();
console.error('Failed to refresh token', error);
return Promise.reject(error);
},
);

export default client;
Loading