Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

서술형 커뮤니티화 #114

Merged
merged 35 commits into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
78c0939
:construction: Work in progress
Kim-Hyunjo Sep 9, 2023
6fa280d
:bento: Add button svgs
Kim-Hyunjo Nov 26, 2023
d806194
:recycle: Seperate description box
Kim-Hyunjo Nov 26, 2023
434c3f6
:art: Seperate PostBox
Kim-Hyunjo Nov 26, 2023
2fa22ff
:lipstick: Update Postbox UI
Kim-Hyunjo Nov 26, 2023
eb81aae
:lipstick: Give font-family to every tag
Kim-Hyunjo Nov 26, 2023
ffc35ac
:lipstick: Add PostInput
Kim-Hyunjo Nov 26, 2023
6c597b5
:lipstick: Add xsmall button size
Kim-Hyunjo Nov 26, 2023
b540c15
:lipstick: Add CommentInput
Kim-Hyunjo Nov 26, 2023
aa57a08
:sparkles: Add addCommentButton
Kim-Hyunjo Nov 26, 2023
7fc36b8
:sparkles: Add api functions
Kim-Hyunjo Nov 26, 2023
b2c84ea
:lipstick: Update addComment button font
Kim-Hyunjo Nov 26, 2023
e5b5562
:construction: Adding community post add api
Kim-Hyunjo Nov 28, 2023
e51748b
:sparkles: Add bookmark, like feature
Kim-Hyunjo Nov 29, 2023
7ccb785
:sparkles: Add answerCount to problemTitle
Kim-Hyunjo Nov 29, 2023
36b60af
:sparkles: Refetch posts when new post added
Kim-Hyunjo Nov 30, 2023
c19a8c1
:sparkles: Add addComment mutation
Kim-Hyunjo Nov 30, 2023
f80ee4c
:sparkles: Add likePost mutations
Kim-Hyunjo Nov 30, 2023
dbea2aa
:lipstick: Update my input UI
Kim-Hyunjo Dec 10, 2023
e1c7097
:hammer: Sort comments by createdAt
Kim-Hyunjo Dec 10, 2023
dab198d
:hammer: Block like, bookmark button when not login
Kim-Hyunjo Dec 10, 2023
3b46d41
:hammer: Show post input when logined
Kim-Hyunjo Dec 10, 2023
27870dd
:hammer: Show comment input when logined
Kim-Hyunjo Dec 10, 2023
db9fe70
Merge branch 'dev' into feat/428
Kim-Hyunjo Dec 10, 2023
6bea535
:mute: Remove console.log
Kim-Hyunjo Dec 17, 2023
c94cfd3
:recycle: Add isLogin util function
Kim-Hyunjo Dec 17, 2023
52aee35
:pencil2: Fix AUTHORIZTION -> AUTHORIZATION
Kim-Hyunjo Dec 17, 2023
09196f1
:bulb: Update Link to profile page into comment
Kim-Hyunjo Dec 17, 2023
86404a1
:wheelchair: Show metaTag keyword only truthy
Kim-Hyunjo Dec 17, 2023
b536499
:adhesive_bandage: Remove unneccesary div when community post does no…
Kim-Hyunjo Dec 17, 2023
5ab0049
🔀 Merge branch 'dev' into feat/428
Kim-Hyunjo Dec 17, 2023
82661df
:sparkles: Link nickname with user profile page
Kim-Hyunjo Jan 14, 2024
44da20d
:recycle: Add getSingleInputValueOnSubmit util function
Kim-Hyunjo Jan 14, 2024
0e35c7c
:heavy_plus_sign: Add vitest
Kim-Hyunjo Jan 14, 2024
37dfbf6
:white_check_mark: Add parseDateTime util test
Kim-Hyunjo Jan 14, 2024
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
1,980 changes: 1,892 additions & 88 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"build": "npm run build --workspace=service",
"build:admin": "npm run build --workspace=admin",
"lint": "eslint 'packages/**/src/**/*.{ts,tsx,js,jsx}'",
"lint-diff": "eslint $(git diff --name-only --diff-filter=duxb origin/$BASE origin/$HEAD | grep -E '\\.((j|t)sx?)$' | xargs)"
"lint-diff": "eslint $(git diff --name-only --diff-filter=duxb origin/$BASE origin/$HEAD | grep -E '\\.((j|t)sx?)$' | xargs)",
"test": "npm run test --workspace=service"
},
"workspaces": [
"packages/service",
Expand Down
6 changes: 3 additions & 3 deletions packages/admin/src/api/apiClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import axios from 'axios';
import { getUserInfo } from 'auth/utils/userInfo';
import { parseJwt } from 'auth/utils/parseJwt';
import { AUTHORIZTION, BEARER_TOKEN } from 'auth/constants';
import { AUTHORIZATION, BEARER_TOKEN } from 'auth/constants';
import { authApiWrapper } from './wrapper/auth/authApiWrapper';

const apiClient = axios.create({
Expand All @@ -13,7 +13,7 @@ const userInfo = getUserInfo();
if (userInfo) {
const token: string | null | undefined = userInfo.accessToken;
if (typeof token === 'string') {
apiClient.defaults.headers.common[AUTHORIZTION] = BEARER_TOKEN(token);
apiClient.defaults.headers.common[AUTHORIZATION] = BEARER_TOKEN(token);
}
}

Expand All @@ -30,7 +30,7 @@ apiClient.interceptors.response.use(
const jwt = parseJwt(userInfo.accessToken);
if (jwt?.exp && Date.now() >= jwt.exp * 1000) {
authApiWrapper.refresh()?.then((newAccessToken) => {
originalRequest.headers[AUTHORIZTION] = BEARER_TOKEN(newAccessToken);
originalRequest.headers[AUTHORIZATION] = BEARER_TOKEN(newAccessToken);
return apiClient(originalRequest);
});
}
Expand Down
6 changes: 3 additions & 3 deletions packages/admin/src/api/wrapper/auth/authApiWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { getUserInfo, setUserInfo } from 'auth/utils/userInfo';
import apiClient from '../../apiClient';
import { API_URL } from '../../../constants/apiUrl';
import { AUTHORIZTION, BEARER_TOKEN, ROLES } from 'auth/constants';
import { AUTHORIZATION, BEARER_TOKEN, ROLES } from 'auth/constants';

interface ILoginRequest {
email: string;
Expand All @@ -18,10 +18,10 @@
alert('권한 없음');
throw new Error('권한 없음');
}
apiClient.defaults.headers.common[AUTHORIZTION] = BEARER_TOKEN(res.data.accessToken);
apiClient.defaults.headers.common[AUTHORIZATION] = BEARER_TOKEN(res.data.accessToken);
return res.data;
},
(err) => {

Check warning on line 24 in packages/admin/src/api/wrapper/auth/authApiWrapper.ts

View workflow job for this annotation

GitHub Actions / check

'err' is defined but never used

Check warning on line 24 in packages/admin/src/api/wrapper/auth/authApiWrapper.ts

View workflow job for this annotation

GitHub Actions / check

'err' is defined but never used. Allowed unused args must match /^_/u
alert('로그인 실패');
throw new Error('로그인 실패');
},
Expand All @@ -40,7 +40,7 @@
.then(
(res: { data: { accessToken: string } }) => {
const newAccessToken = res.data.accessToken;
apiClient.defaults.headers.common[AUTHORIZTION] = BEARER_TOKEN(newAccessToken);
apiClient.defaults.headers.common[AUTHORIZATION] = BEARER_TOKEN(newAccessToken);
setUserInfo({ ...userInfo, accessToken: newAccessToken });
return res.data.accessToken;
},
Expand Down
6 changes: 3 additions & 3 deletions packages/admin/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import apiClient from '../api/apiClient';
import { AUTHORIZTION, BEARER_TOKEN, USER_INFO } from 'auth/constants';
import { AUTHORIZATION, BEARER_TOKEN, USER_INFO } from 'auth/constants';

export const roundToSecondDigit = (num: number) => Math.round(num * 100) / 100;

Expand All @@ -13,13 +13,13 @@
}
};

export const isNumeric = (value: any) => {

Check warning on line 16 in packages/admin/src/utils/index.ts

View workflow job for this annotation

GitHub Actions / check

Unexpected any. Specify a different type
return !isNaN(Number(value));
};

export function setLogout() {
localStorage.removeItem(USER_INFO);
delete apiClient.defaults.headers.common[AUTHORIZTION];
delete apiClient.defaults.headers.common[AUTHORIZATION];
}

export const setTokenHeader = () => {
Expand All @@ -29,7 +29,7 @@
const userInfo = JSON.parse(userInfoString);
const token: string | null | undefined = userInfo.accessToken;
if (typeof token === 'string') {
apiClient.defaults.headers.common[AUTHORIZTION] = BEARER_TOKEN(token);
apiClient.defaults.headers.common[AUTHORIZATION] = BEARER_TOKEN(token);
}
}
} catch (e) {
Expand Down
8 changes: 4 additions & 4 deletions packages/auth/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const USER_INFO = "userInfo";
export const AUTHORIZTION = "Authorization";
export const USER_INFO = 'userInfo';
export const AUTHORIZATION = 'Authorization';
export const BEARER_TOKEN = (token: string) => `Bearer ${token}`;
export const ROLES = {
ROLE_ADMIN: "ROLE_ADMIN",
ROLE_USER: "ROLE_USER",
ROLE_ADMIN: 'ROLE_ADMIN',
ROLE_USER: 'ROLE_USER',
} as const;
2 changes: 2 additions & 0 deletions packages/auth/utils/userInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ export const getUserInfo = (): IUserInfo | null => {
export const removeUserInfo = () => {
localStorage.removeItem(USER_INFO);
};

export const isLogin = () => !!getUserInfo()?.id;
4 changes: 3 additions & 1 deletion packages/service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"scripts": {
"dev": "vite --mode development --port 3123",
"build": "tsc && vite build --mode production",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -44,6 +45,7 @@
"rehype-highlight": "^5.0.2",
"remark-gfm": "^3.0.1",
"vite-plugin-svgr": "^2.2.0",
"vitest": "^1.2.0",
"zustand": "^4.0.0-rc.1"
},
"devDependencies": {
Expand Down
10 changes: 10 additions & 0 deletions packages/service/src/Component/Button/TextButton/style.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ export const textButtonSizeStyle = styleVariants({
borderRadius: '8px',
},
],
xsmall: [
{
width: '3.5rem',
height: '1.5rem',
fontWeight: '500',
fontSize: '0.75rem',
lineHeight: '1rem',
borderRadius: '4px',
},
],
});

export const unactivatedStyle = style({
Expand Down
3 changes: 3 additions & 0 deletions packages/service/src/Organism/ProblemTitle/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface IProblemDetail {
score: string;
correctSubmission: string;
correctUserCnt: string;
answerCount: string;
}

interface IProblemDetailDescription {
Expand Down Expand Up @@ -61,6 +62,8 @@ const problemDetailMap: Record<
isStringNotEmpty(num) ? { label: '정답', value: num, unit: '' } : null,
correctUserCnt: (num: string | undefined) =>
isStringNotEmpty(num) ? { label: '맞힌 사람 수', value: num, unit: '명' } : null,
answerCount: (num: string | undefined) =>
isStringNotEmpty(num) ? { label: '답변 수', value: num, unit: '' } : null,
};

function ProblemTitle(props: IProblemTitle) {
Expand Down
4 changes: 2 additions & 2 deletions packages/service/src/Page/CallbackPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import apiClient from '../../api/apiClient';
import { authApiWrapper } from '../../api/wrapper/auth/authApiWrapper';
import { AUTHORIZTION, BEARER_TOKEN } from 'auth/constants';
import { AUTHORIZATION, BEARER_TOKEN } from 'auth/constants';
import { URL } from '../../constants/url';
import { setUserInfo } from 'auth/utils/userInfo';

Expand All @@ -15,7 +15,7 @@ const CallbackPage = () => {

if (!token) return;
authApiWrapper.getUserData(token).then((res) => {
apiClient.defaults.headers.common[AUTHORIZTION] = BEARER_TOKEN(token);
apiClient.defaults.headers.common[AUTHORIZATION] = BEARER_TOKEN(token);
setUserInfo({ ...res.data, accessToken: token });
navigate(URL.MAIN);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { boxStyle } from './style.css';

type Props = {
children: React.ReactNode;
className?: string;
};

const Box = ({ children, className }: Props) => {
return <div className={`${boxStyle} ${className}`}>{children}</div>;
};

export default Box;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { style } from '@vanilla-extract/css';
import { themeColors } from '../../../../../styles/theme.css';

export const boxStyle = style({
padding: '24px',
border: `1px solid ${themeColors.line.d}`,
borderRadius: '8px',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { QueryObserverResult, useMutation } from 'react-query';
import { communityApiWrapper } from '../../../../../api/wrapper/community/communityApiWrapper';
import { TextButton } from '../../../../../Component/Button';
import { LongProblemPost } from '../../../../../types/api/community';
import { getSingleInputValueOnSubmit } from '../../../../../utils/getSingleInputValueOnSubmit';
import { textareaStyle, textareaWrapStyle, wrapStyle } from './style.css';

type Props = {
postId: number;
refetchCommunityPost: () => Promise<QueryObserverResult<LongProblemPost[], unknown>>;
};

const CommentInput = ({ postId, refetchCommunityPost }: Props) => {
const { mutate: addComment } = useMutation(communityApiWrapper.addComment, {
onSuccess: () => refetchCommunityPost(),
});
return (
<form
onSubmit={(e) => {
const formElement = e.target as HTMLFormElement;
const content = getSingleInputValueOnSubmit(e, 'comment-input');
if (!content) return;
addComment({ postId: postId, content });
formElement.reset();
}}
>
<div className={wrapStyle}>
<div className={textareaWrapStyle}>
<textarea className={textareaStyle} id='comment-input' name='comment-input' />
</div>
<TextButton theme='primary' size='xsmall' type='submit'>
제출하기
</TextButton>
</div>
</form>
);
};
export default CommentInput;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { style } from '@vanilla-extract/css';
import { themeColors } from '../../../../../styles/theme.css';

export const wrapStyle = style({
display: 'flex',
alignItems: 'center',
gap: '1rem',
marginTop: '1rem',
});

export const textareaWrapStyle = style({
flex: 1,
padding: '0.5rem',
border: `1px solid ${themeColors.line.d}`,
borderRadius: '4px',
});

export const textareaStyle = style({
width: '100%',
fontSize: '1rem',
fontWeight: 400,
lineHeight: '1.25rem',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import Box from '../Box';
import IconButton from '../IconButton';
import { ReactComponent as StarIcon } from '../../../../../assets/icons/star.svg';
import { ReactComponent as ThumbUpIcon } from '../../../../../assets/icons/thumb_up.svg';
import { COLOR } from '../../../../../constants/color';
import { buttonWrap, descriptionWrap, topWrap, wrap } from './style.css';
import { QueryObserverResult, useMutation } from 'react-query';
import { problemApiWrapper } from '../../../../../api/wrapper/problem/problemApiWrapper';
import { ILongProblemDetailResponseData } from '../../../../../types/api/problem';
import { communityApiWrapper } from '../../../../../api/wrapper/community/communityApiWrapper';
import PostInput from '../PostInput';
import { LongProblemPost } from '../../../../../types/api/community';
import { isLogin } from 'auth/utils/userInfo';
import { getSingleInputValueOnSubmit } from '../../../../../utils/getSingleInputValueOnSubmit';

type Props = {
id: string;
description: string;
isLiked: boolean;
likeCount: number;
isBookmarked: boolean;
bookmarkCount: number;
refetchProblemDetail: () => Promise<QueryObserverResult<ILongProblemDetailResponseData, unknown>>;
refetchCommunityPost: () => Promise<QueryObserverResult<LongProblemPost[], unknown>>;
};

const DescriptionBox = ({
id,
description,
isLiked,
likeCount,
isBookmarked,
bookmarkCount,
refetchProblemDetail,
refetchCommunityPost,
}: Props) => {
const { mutate: likeProblem } = useMutation(
['likeProblem', id],
() => problemApiWrapper.likeProblem({ problemId: id }),
{ onSuccess: () => refetchProblemDetail() },
);
const { mutate: bookmarkProblem } = useMutation(
['bookmarkProblem', id],
() => problemApiWrapper.bookmarkProblem({ problemId: id }),
{ onSuccess: () => refetchProblemDetail() },
);
const { mutate: addPost } = useMutation(communityApiWrapper.addPost, {
onSuccess: () => refetchCommunityPost(),
});

return (
<Box className={wrap}>
<div className={topWrap}>
<div>문제 설명</div>
<div className={buttonWrap}>
<IconButton
text={bookmarkCount.toString()}
onClick={() => {
bookmarkProblem();
}}
>
<StarIcon fill={isBookmarked ? COLOR.PRIMARY : COLOR.GRAY} width='2rem' height='2rem' />
</IconButton>
<IconButton
text={likeCount.toString()}
onClick={() => {
likeProblem();
}}
>
<ThumbUpIcon fill={isLiked ? COLOR.PRIMARY : COLOR.GRAY} width='2rem' height='2rem' />
</IconButton>
</div>
</div>
<div className={descriptionWrap}>{description}</div>
{isLogin() && (
<form
onSubmit={(e) => {
const formElement = e.target as HTMLFormElement;
const content = getSingleInputValueOnSubmit(e, 'post-input');
if (!content) return;
addPost({ problemId: parseInt(id), content });
formElement.reset();
}}
>
<PostInput />
</form>
)}
</Box>
);
};

export default DescriptionBox;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { style } from '@vanilla-extract/css';

export const wrap = style({
marginTop: '24px',
});

export const topWrap = style({
display: 'flex',
justifyContent: 'space-between',
fontSize: '1.75rem',
fontWeight: 700,
lineHeight: '2rem',
});

export const buttonWrap = style({
display: 'flex',
gap: '8px',
});

export const descriptionWrap = style({
marginTop: '12px',
fontSize: '1.25rem',
fontStyle: 'normal',
fontWeight: 400,
lineHeight: '1.5rem',
whiteSpace: 'pre-wrap',
});
Loading