From 995c6bec327fe03df21ab40b4b18f58f67cfe6f4 Mon Sep 17 00:00:00 2001 From: carrick Date: Tue, 19 Mar 2024 18:08:40 +0900 Subject: [PATCH 01/52] fix: markdownEditor for edit post --- src/containers/write/MarkdownEditorContainer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/write/MarkdownEditorContainer.tsx b/src/containers/write/MarkdownEditorContainer.tsx index 40e7e74f..db27fe42 100644 --- a/src/containers/write/MarkdownEditorContainer.tsx +++ b/src/containers/write/MarkdownEditorContainer.tsx @@ -188,7 +188,7 @@ const MarkdownEditorContainer: React.FC = () => { } // tempsaving unreleased post: - if (isTemp) { + if (isTemp && postId) { await editPost({ variables: { id: postId, From e62f5606671c8ac43c63d9e1b73117cad08873f1 Mon Sep 17 00:00:00 2001 From: carrick Date: Tue, 7 May 2024 22:45:09 +0900 Subject: [PATCH 02/52] fix: allow attributes for img tag #536 --- src/components/common/MarkdownRender.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/common/MarkdownRender.tsx b/src/components/common/MarkdownRender.tsx index ebd9b363..febe9ecc 100644 --- a/src/components/common/MarkdownRender.tsx +++ b/src/components/common/MarkdownRender.tsx @@ -170,7 +170,7 @@ function filter(html: string) { ], allowedAttributes: { a: ['href', 'name', 'target'], - img: ['src'], + img: ['src', 'alt', 'width', 'height'], iframe: ['src', 'allow', 'allowfullscreen', 'scrolling', 'class'], '*': ['class', 'id', 'aria-hidden'], span: ['style'], From e29685fa4744694f9cc48b9b1be5603cee14ca74 Mon Sep 17 00:00:00 2001 From: carrick Date: Tue, 21 May 2024 10:43:55 +0900 Subject: [PATCH 03/52] fix: add suffix to handle social id --- src/components/common/UserProfile.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/common/UserProfile.tsx b/src/components/common/UserProfile.tsx index 51258293..ea1b4545 100644 --- a/src/components/common/UserProfile.tsx +++ b/src/components/common/UserProfile.tsx @@ -176,6 +176,8 @@ const UserProfile: React.FC = ({ const velogUrl = `/@${username}/posts`; + const getSocialId = (link: string) => link.split('/').reverse()[0]; + return (
@@ -201,7 +203,7 @@ const UserProfile: React.FC = ({ {github && ( = ({ )} {twitter && ( = ({ )} {facebook && ( Date: Fri, 19 Jul 2024 08:02:27 +0900 Subject: [PATCH 04/52] fix: add filter for nullalble profile --- src/components/post/PostCommentsList.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/post/PostCommentsList.tsx b/src/components/post/PostCommentsList.tsx index c3a4c953..b7016728 100644 --- a/src/components/post/PostCommentsList.tsx +++ b/src/components/post/PostCommentsList.tsx @@ -20,15 +20,17 @@ const PostCommentsList: React.FC = ({ }) => { return ( - {comments.map((comment) => ( - - ))} + {comments + .filter((comment) => !!comment.user?.profile) + .map((comment) => ( + + ))} ); }; From 681402b939ed9c2ffbb8e6f06036bcd17b492fab Mon Sep 17 00:00:00 2001 From: carrick Date: Mon, 5 Aug 2024 07:26:55 +0900 Subject: [PATCH 05/52] fix: regex for support es5 --- src/components/write/WriteMarkdownEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/write/WriteMarkdownEditor.tsx b/src/components/write/WriteMarkdownEditor.tsx index 46e45543..75a046b8 100644 --- a/src/components/write/WriteMarkdownEditor.tsx +++ b/src/components/write/WriteMarkdownEditor.tsx @@ -1121,7 +1121,7 @@ const checker = { }, codesandbox: (text: string) => { const regex = - /^$/s; + /^$/; const result = regex.exec(text); if (!result) return null; return result[1]; From e54b5da8a68e50a743e0212f55d7cc4ef0e6fbc1 Mon Sep 17 00:00:00 2001 From: carrick Date: Tue, 6 Aug 2024 10:04:25 +0900 Subject: [PATCH 06/52] fix: improve temp save functionality --- src/components/write/WriteMarkdownEditor.tsx | 1 - .../write/MarkdownEditorContainer.tsx | 40 +++++++++---------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/components/write/WriteMarkdownEditor.tsx b/src/components/write/WriteMarkdownEditor.tsx index 75a046b8..1386475f 100644 --- a/src/components/write/WriteMarkdownEditor.tsx +++ b/src/components/write/WriteMarkdownEditor.tsx @@ -1129,7 +1129,6 @@ const checker = { codepen: (text: string) => { const regex = /^ = () => { tags, } = useSelector((state: RootState) => state.write); const uncachedClient = useUncachedApolloClient(); - const [writePost, { loading: writePostLoading }] = + const [writePost, { loading: isWritePostLoading }] = useMutation(WRITE_POST, { client: uncachedClient, }); @@ -70,12 +70,10 @@ const MarkdownEditorContainer: React.FC = () => { const titleRef = useRef(title); const [createPostHistory] = useMutation(CREATE_POST_HISTORY); - const [editPost, { loading: editPostLoading }] = useMutation( - EDIT_POST, - { + const [editPost, { loading: isEditPostLoading }] = + useMutation(EDIT_POST, { client: uncachedClient, - }, - ); + }); const [lastSavedData, setLastSavedData] = useState({ title: initialTitle, @@ -152,7 +150,9 @@ const MarkdownEditorContainer: React.FC = () => { const onTempSave = useCallback( async (notify?: boolean) => { - if (writePostLoading || editPostLoading) return; + console.log('onTempSave'); + + if (isWritePostLoading || isEditPostLoading) return; if (!title || !markdown) { toast.error('제목 또는 내용이 비어있습니다.'); return; @@ -164,6 +164,7 @@ const MarkdownEditorContainer: React.FC = () => { }; if (!postId) { + console.log('writePost'); const response = await writePost({ variables: { title, @@ -206,7 +207,6 @@ const MarkdownEditorContainer: React.FC = () => { }, }); notifySuccess(); - return; } // tempsaving released post: @@ -243,8 +243,8 @@ const MarkdownEditorContainer: React.FC = () => { tags, title, writePost, - writePostLoading, - editPostLoading, + isWritePostLoading, + isEditPostLoading, ], ); @@ -308,16 +308,16 @@ const MarkdownEditorContainer: React.FC = () => { useEffect(() => { const changed = !shallowEqual(lastSavedData, { title, body: markdown }); - if (changed) { - const timeoutId = setTimeout(() => { - if (!postId && !title && markdown.length < 30) return; - onTempSave(true); - }, 10 * 1000); - - return () => { - clearTimeout(timeoutId); - }; - } + if (!changed) return; + + const timeoutId = setTimeout(() => { + if (!postId && !title && markdown.length < 30) return; + onTempSave(true); + }, 1000 * 10); + + return () => { + clearTimeout(timeoutId); + }; }, [title, postId, onTempSave, lastSavedData, markdown]); useSaveHotKey(() => onTempSave(true)); From 4b98e9b03e2c5d72258572d4f511b9601a38d867 Mon Sep 17 00:00:00 2001 From: carrick Date: Tue, 6 Aug 2024 11:02:49 +0900 Subject: [PATCH 07/52] fix: Improve temp save functionality and remove notification when auto save --- src/containers/write/MarkdownEditorContainer.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/containers/write/MarkdownEditorContainer.tsx b/src/containers/write/MarkdownEditorContainer.tsx index 3127a40c..b2117061 100644 --- a/src/containers/write/MarkdownEditorContainer.tsx +++ b/src/containers/write/MarkdownEditorContainer.tsx @@ -150,7 +150,10 @@ const MarkdownEditorContainer: React.FC = () => { const onTempSave = useCallback( async (notify?: boolean) => { - console.log('onTempSave'); + const notifySuccess = () => { + if (!notify) return; + toast.success('포스트가 임시저장되었습니다.'); + }; if (isWritePostLoading || isEditPostLoading) return; if (!title || !markdown) { @@ -158,13 +161,7 @@ const MarkdownEditorContainer: React.FC = () => { return; } - const notifySuccess = () => { - if (!notify) return; - toast.success('포스트가 임시저장되었습니다.'); - }; - if (!postId) { - console.log('writePost'); const response = await writePost({ variables: { title, @@ -189,7 +186,7 @@ const MarkdownEditorContainer: React.FC = () => { } // tempsaving unreleased post: - if (isTemp && postId) { + if (postId) { await editPost({ variables: { id: postId, @@ -282,6 +279,7 @@ const MarkdownEditorContainer: React.FC = () => { dispatch(setWritePostId(id)); history.replace(`/write?id=${id}`); } + if (!id) return; const url = URL.createObjectURL(file); @@ -312,7 +310,7 @@ const MarkdownEditorContainer: React.FC = () => { const timeoutId = setTimeout(() => { if (!postId && !title && markdown.length < 30) return; - onTempSave(true); + onTempSave(false); }, 1000 * 10); return () => { From f9d2c37fdab2c8ad6afdc0ff903c9aaedfd04f3d Mon Sep 17 00:00:00 2001 From: carrick Date: Tue, 6 Aug 2024 12:46:46 +0900 Subject: [PATCH 08/52] chore: Refactor MarkdownEditorContainer.tsx for improved temp save functionality --- .../write/MarkdownEditorContainer.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/containers/write/MarkdownEditorContainer.tsx b/src/containers/write/MarkdownEditorContainer.tsx index b2117061..911d7ca7 100644 --- a/src/containers/write/MarkdownEditorContainer.tsx +++ b/src/containers/write/MarkdownEditorContainer.tsx @@ -279,7 +279,7 @@ const MarkdownEditorContainer: React.FC = () => { dispatch(setWritePostId(id)); history.replace(`/write?id=${id}`); } - + if (!id) return; const url = URL.createObjectURL(file); @@ -306,16 +306,16 @@ const MarkdownEditorContainer: React.FC = () => { useEffect(() => { const changed = !shallowEqual(lastSavedData, { title, body: markdown }); - if (!changed) return; - - const timeoutId = setTimeout(() => { - if (!postId && !title && markdown.length < 30) return; - onTempSave(false); - }, 1000 * 10); - - return () => { - clearTimeout(timeoutId); - }; + if (changed) { + const timeoutId = setTimeout(() => { + if (!postId && !title && markdown.length < 30) return; + onTempSave(true); + }, 10 * 1000); + + return () => { + clearTimeout(timeoutId); + }; + } }, [title, postId, onTempSave, lastSavedData, markdown]); useSaveHotKey(() => onTempSave(true)); From 4c9e0fc361e3fe1aa74345af2ab6dbd0b3bec610 Mon Sep 17 00:00:00 2001 From: carrick Date: Mon, 12 Aug 2024 09:12:26 +0900 Subject: [PATCH 09/52] chore: Update redirect URL after deleting series in SeriesPosts.tsx --- src/containers/velog/SeriesPosts.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/containers/velog/SeriesPosts.tsx b/src/containers/velog/SeriesPosts.tsx index 4ad34970..7843549b 100644 --- a/src/containers/velog/SeriesPosts.tsx +++ b/src/containers/velog/SeriesPosts.tsx @@ -59,15 +59,17 @@ const SeriesPosts: React.FC = ({ username, urlSlug }) => { id: data?.series?.id, }, }); + await client.resetStore(); - toast.success('시리즈가 삭제되었습니다.'); - // history.replace(`/@${username}/series/`); - window.location.href = `${process.env + + const redirect = `${process.env .REACT_APP_CLIENT_V3_HOST!}/@${username}/series`; + window.location.href = redirect; } catch (e) { toast.error('시리즈 삭제 실패'); } }; + const onCancelRemove = () => { setAskRemove(false); }; From 7dd8c1ae4514d3ac5554ddaf0958cbec952974e0 Mon Sep 17 00:00:00 2001 From: carrick Date: Wed, 14 Aug 2024 17:05:55 +0900 Subject: [PATCH 10/52] feat: Add isPrivate, isTemp flag --- src/containers/write/MarkdownEditorContainer.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/containers/write/MarkdownEditorContainer.tsx b/src/containers/write/MarkdownEditorContainer.tsx index 911d7ca7..510e651c 100644 --- a/src/containers/write/MarkdownEditorContainer.tsx +++ b/src/containers/write/MarkdownEditorContainer.tsx @@ -59,6 +59,7 @@ const MarkdownEditorContainer: React.FC = () => { initialBody, initialTitle, tags, + isPrivate, } = useSelector((state: RootState) => state.write); const uncachedClient = useUncachedApolloClient(); const [writePost, { loading: isWritePostLoading }] = @@ -193,8 +194,8 @@ const MarkdownEditorContainer: React.FC = () => { title, body: markdown, is_markdown: true, - is_temp: true, - is_private: false, + is_temp: isTemp, + is_private: isPrivate, url_slug: escapeForUrl(title), thumbnail: null, meta: {}, @@ -233,7 +234,6 @@ const MarkdownEditorContainer: React.FC = () => { dispatch, editPost, history, - isTemp, lastSavedData, markdown, postId, @@ -242,6 +242,7 @@ const MarkdownEditorContainer: React.FC = () => { writePost, isWritePostLoading, isEditPostLoading, + isTemp, ], ); @@ -310,7 +311,7 @@ const MarkdownEditorContainer: React.FC = () => { const timeoutId = setTimeout(() => { if (!postId && !title && markdown.length < 30) return; onTempSave(true); - }, 10 * 1000); + }, 1000 * 10); return () => { clearTimeout(timeoutId); From c090119cb5ab4e020d93d8c24fa25846ea5703c1 Mon Sep 17 00:00:00 2001 From: carrick Date: Wed, 14 Aug 2024 18:53:35 +0900 Subject: [PATCH 11/52] fix: edit post variables --- src/containers/write/MarkdownEditorContainer.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/containers/write/MarkdownEditorContainer.tsx b/src/containers/write/MarkdownEditorContainer.tsx index 510e651c..dd99a053 100644 --- a/src/containers/write/MarkdownEditorContainer.tsx +++ b/src/containers/write/MarkdownEditorContainer.tsx @@ -60,6 +60,7 @@ const MarkdownEditorContainer: React.FC = () => { initialTitle, tags, isPrivate, + selectedSeries, } = useSelector((state: RootState) => state.write); const uncachedClient = useUncachedApolloClient(); const [writePost, { loading: isWritePostLoading }] = @@ -199,7 +200,7 @@ const MarkdownEditorContainer: React.FC = () => { url_slug: escapeForUrl(title), thumbnail: null, meta: {}, - series_id: null, + series_id: selectedSeries?.id || null, tags, token: null, }, From ed4fcd87dd751d388aec283611e6289ae8f22691 Mon Sep 17 00:00:00 2001 From: carrick Date: Wed, 14 Aug 2024 19:13:19 +0900 Subject: [PATCH 12/52] fix: add dependency in useEffect array --- src/containers/write/ActiveEditor.tsx | 1 + src/containers/write/MarkdownEditorContainer.tsx | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/containers/write/ActiveEditor.tsx b/src/containers/write/ActiveEditor.tsx index 080f9cf0..53d6285b 100644 --- a/src/containers/write/ActiveEditor.tsx +++ b/src/containers/write/ActiveEditor.tsx @@ -77,6 +77,7 @@ const ActiveEditor: React.FC = () => { useEffect(() => { if (!post) return; if (initialized.current) return; + console.log('post.series', post.series); dispatch( prepareEdit({ id: post.id, diff --git a/src/containers/write/MarkdownEditorContainer.tsx b/src/containers/write/MarkdownEditorContainer.tsx index dd99a053..1dcf19fc 100644 --- a/src/containers/write/MarkdownEditorContainer.tsx +++ b/src/containers/write/MarkdownEditorContainer.tsx @@ -244,6 +244,8 @@ const MarkdownEditorContainer: React.FC = () => { isWritePostLoading, isEditPostLoading, isTemp, + isPrivate, + selectedSeries, ], ); From a86f21c5c53fb93fe14ba82b19d503766fcf7a7b Mon Sep 17 00:00:00 2001 From: carrick Date: Wed, 14 Aug 2024 19:26:14 +0900 Subject: [PATCH 13/52] chore: clear console --- src/containers/write/ActiveEditor.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/containers/write/ActiveEditor.tsx b/src/containers/write/ActiveEditor.tsx index 53d6285b..080f9cf0 100644 --- a/src/containers/write/ActiveEditor.tsx +++ b/src/containers/write/ActiveEditor.tsx @@ -77,7 +77,6 @@ const ActiveEditor: React.FC = () => { useEffect(() => { if (!post) return; if (initialized.current) return; - console.log('post.series', post.series); dispatch( prepareEdit({ id: post.id, From b5ea8ade4fa4aefbded64c4540a8ff9337360e96 Mon Sep 17 00:00:00 2001 From: carrick Date: Tue, 20 Aug 2024 08:26:35 +0900 Subject: [PATCH 14/52] chore: Add isSkip state for skipping query when remove series --- src/containers/velog/SeriesPosts.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/containers/velog/SeriesPosts.tsx b/src/containers/velog/SeriesPosts.tsx index 7843549b..7980c9f4 100644 --- a/src/containers/velog/SeriesPosts.tsx +++ b/src/containers/velog/SeriesPosts.tsx @@ -40,6 +40,7 @@ const SeriesPosts: React.FC = ({ username, urlSlug }) => { const user = useUser(); const isOwnSeries = user && user.username === username; const [askRemove, setAskRemove] = useState(false); + const [isSkip, setIsSkip] = useState(false); const [removeSeries] = useMutation(REMOVE_SERIES); const { data } = useQuery(GET_SERIES, { variables: { @@ -47,6 +48,7 @@ const SeriesPosts: React.FC = ({ username, urlSlug }) => { url_slug: urlSlug, }, fetchPolicy: 'cache-and-network', + skip: isSkip, }); const client = useApolloClient(); @@ -54,6 +56,7 @@ const SeriesPosts: React.FC = ({ username, urlSlug }) => { const onAskRemove = () => setAskRemove(true); const onConfirmRemove = async () => { try { + setIsSkip(true); await removeSeries({ variables: { id: data?.series?.id, @@ -64,7 +67,10 @@ const SeriesPosts: React.FC = ({ username, urlSlug }) => { const redirect = `${process.env .REACT_APP_CLIENT_V3_HOST!}/@${username}/series`; - window.location.href = redirect; + + setTimeout(() => { + window.location.href = redirect; + }, 100); } catch (e) { toast.error('시리즈 삭제 실패'); } From e45f87138145d4e9a0137fa543c766919aaf2097 Mon Sep 17 00:00:00 2001 From: carrick Date: Tue, 20 Aug 2024 08:30:30 +0900 Subject: [PATCH 15/52] chore: Refactor series remove --- src/containers/velog/SeriesPosts.tsx | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/containers/velog/SeriesPosts.tsx b/src/containers/velog/SeriesPosts.tsx index 7980c9f4..2a6b117f 100644 --- a/src/containers/velog/SeriesPosts.tsx +++ b/src/containers/velog/SeriesPosts.tsx @@ -57,20 +57,28 @@ const SeriesPosts: React.FC = ({ username, urlSlug }) => { const onConfirmRemove = async () => { try { setIsSkip(true); + setAskRemove(false); + + if (!data?.series?.id) { + throw new Error('Series ID is not available'); + } + await removeSeries({ variables: { - id: data?.series?.id, + id: data.series.id, }, }); await client.resetStore(); - const redirect = `${process.env - .REACT_APP_CLIENT_V3_HOST!}/@${username}/series`; - - setTimeout(() => { - window.location.href = redirect; - }, 100); + toast.success('시리즈가 성공적으로 삭제되었습니다.', { + autoClose: 800, + onClose: () => { + const redirect = `${process.env + .REACT_APP_CLIENT_V3_HOST!}/@${username}/series`; + window.location.href = redirect; + }, + }); } catch (e) { toast.error('시리즈 삭제 실패'); } From a375f052747ab6d7caa766b2670dede28a832057 Mon Sep 17 00:00:00 2001 From: carrick Date: Tue, 20 Aug 2024 09:21:11 +0900 Subject: [PATCH 16/52] fix: improve experience for remove post --- src/components/post/PostHead.tsx | 1 + src/containers/post/PostViewer.tsx | 21 +++++++++++++++++---- src/containers/velog/SeriesPosts.tsx | 8 ++++---- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/components/post/PostHead.tsx b/src/components/post/PostHead.tsx index 143f2b94..9776aa31 100644 --- a/src/components/post/PostHead.tsx +++ b/src/components/post/PostHead.tsx @@ -176,6 +176,7 @@ const PostHead: React.FC = ({ toggleAskRemove(); onRemove(); }; + return (
diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 5406f8f1..7ce37736 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -101,6 +101,7 @@ const PostViewer: React.FC = ({ ); const prefetched = useRef(false); + const [isRemoveLoading, setIsRemoveLoading] = useState(false); const [removePost] = useMutation(REMOVE_POST); const [postView] = useMutation(POST_VIEW); const [likePost, { loading: loadingLike }] = useMutation(LIKE_POST); @@ -252,15 +253,27 @@ const PostViewer: React.FC = ({ const onRemove = async () => { if (!data || !data.post) return; + setIsRemoveLoading(true); try { await removePost({ variables: { id: data.post.id, }, }); - history.push('/'); - await client.resetStore(); - } catch (e) {} + + toast.success('게시글이 성공적으로 삭제되었습니다.', { + autoClose: 800, + onClose: () => { + const redirect = `${process.env + .REACT_APP_CLIENT_V3_HOST!}/@${username}/posts`; + window.location.href = redirect; + }, + }); + } catch (e) { + toast.error('게시글 삭제 실패'); + console.error('Post deletion failed:', e); + setIsRemoveLoading(false); + } }; if (error) { @@ -377,7 +390,7 @@ const PostViewer: React.FC = ({ } }; - if (!data || !data.post) return ; + if (!data || !data.post || isRemoveLoading) return ; const { post } = data; diff --git a/src/containers/velog/SeriesPosts.tsx b/src/containers/velog/SeriesPosts.tsx index 2a6b117f..68c20e9b 100644 --- a/src/containers/velog/SeriesPosts.tsx +++ b/src/containers/velog/SeriesPosts.tsx @@ -40,15 +40,15 @@ const SeriesPosts: React.FC = ({ username, urlSlug }) => { const user = useUser(); const isOwnSeries = user && user.username === username; const [askRemove, setAskRemove] = useState(false); - const [isSkip, setIsSkip] = useState(false); - const [removeSeries] = useMutation(REMOVE_SERIES); + const [removeSeries, { loading: isRemoveSeriesLoading }] = + useMutation(REMOVE_SERIES); const { data } = useQuery(GET_SERIES, { variables: { username, url_slug: urlSlug, }, fetchPolicy: 'cache-and-network', - skip: isSkip, + skip: isRemoveSeriesLoading, }); const client = useApolloClient(); @@ -56,7 +56,6 @@ const SeriesPosts: React.FC = ({ username, urlSlug }) => { const onAskRemove = () => setAskRemove(true); const onConfirmRemove = async () => { try { - setIsSkip(true); setAskRemove(false); if (!data?.series?.id) { @@ -81,6 +80,7 @@ const SeriesPosts: React.FC = ({ username, urlSlug }) => { }); } catch (e) { toast.error('시리즈 삭제 실패'); + console.error('Series deletion failed:', e); } }; From 72208d19d4996f953e051156eab89112472f4887 Mon Sep 17 00:00:00 2001 From: carrick Date: Tue, 20 Aug 2024 10:34:44 +0900 Subject: [PATCH 17/52] chore: Add isSkip state for skipping query when removing series --- src/containers/velog/SeriesPosts.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/containers/velog/SeriesPosts.tsx b/src/containers/velog/SeriesPosts.tsx index 68c20e9b..ba736ee9 100644 --- a/src/containers/velog/SeriesPosts.tsx +++ b/src/containers/velog/SeriesPosts.tsx @@ -40,15 +40,15 @@ const SeriesPosts: React.FC = ({ username, urlSlug }) => { const user = useUser(); const isOwnSeries = user && user.username === username; const [askRemove, setAskRemove] = useState(false); - const [removeSeries, { loading: isRemoveSeriesLoading }] = - useMutation(REMOVE_SERIES); + const [isSkip, setIsSkip] = useState(false); + const [removeSeries] = useMutation(REMOVE_SERIES); const { data } = useQuery(GET_SERIES, { variables: { username, url_slug: urlSlug, }, fetchPolicy: 'cache-and-network', - skip: isRemoveSeriesLoading, + skip: isSkip, }); const client = useApolloClient(); @@ -56,6 +56,7 @@ const SeriesPosts: React.FC = ({ username, urlSlug }) => { const onAskRemove = () => setAskRemove(true); const onConfirmRemove = async () => { try { + setIsSkip(true); setAskRemove(false); if (!data?.series?.id) { From a1f0dde2ce45de7be22dd7ec05e603dfbb06b76d Mon Sep 17 00:00:00 2001 From: carrick Date: Tue, 20 Aug 2024 11:08:56 +0900 Subject: [PATCH 18/52] feat: Add support for Dockerfile syntax highlighting --- src/components/write/WriteMarkdownEditor.tsx | 1 + src/lib/remark/prismPlugin.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/write/WriteMarkdownEditor.tsx b/src/components/write/WriteMarkdownEditor.tsx index 1386475f..eba9ff47 100644 --- a/src/components/write/WriteMarkdownEditor.tsx +++ b/src/components/write/WriteMarkdownEditor.tsx @@ -21,6 +21,7 @@ require('codemirror/mode/markdown/markdown'); require('codemirror/mode/javascript/javascript'); require('codemirror/mode/jsx/jsx'); require('codemirror/addon/display/placeholder'); +require('codemirror/mode/dockerfile/dockerfile'); export interface MarkdownEditorProps { onChangeMarkdown: (markdown: string) => void; diff --git a/src/lib/remark/prismPlugin.js b/src/lib/remark/prismPlugin.js index 1eee6d74..2007f8ec 100644 --- a/src/lib/remark/prismPlugin.js +++ b/src/lib/remark/prismPlugin.js @@ -28,6 +28,7 @@ import 'prismjs/components/prism-ruby.min'; import 'prismjs/components/prism-rust.min'; import 'prismjs/components/prism-yaml.min'; import 'prismjs/components/prism-dart'; +import 'prismjs/components/prism-docker'; import { ssrEnabled } from '../utils'; @@ -73,5 +74,5 @@ export default function attacher({ include, exclude } = {}) { // 'language-' + lang, // ]; } - return ast => visit(ast, 'code', visitor); + return (ast) => visit(ast, 'code', visitor); } From 3dd0f9a152229538c28bc7a8883345357854c432 Mon Sep 17 00:00:00 2001 From: carrick Date: Wed, 28 Aug 2024 10:02:39 +0900 Subject: [PATCH 19/52] feat: Update RegisterForm component labels and placeholders --- src/components/register/RegisterForm.tsx | 6 +++--- src/containers/register/RegisterFormContainer.tsx | 6 +++--- src/lib/api/apiClient.ts | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/register/RegisterForm.tsx b/src/components/register/RegisterForm.tsx index 1ffaefcf..44a9bb73 100644 --- a/src/components/register/RegisterForm.tsx +++ b/src/components/register/RegisterForm.tsx @@ -103,8 +103,8 @@ const RegisterForm: React.FC = ({ @@ -123,7 +123,7 @@ const RegisterForm: React.FC = ({ name="username" onChange={onChange} label="사용자 ID" - placeholder="새 사용자 ID를 입력하세요" + placeholder="새 사용자 ID를 입력하세요. 변경이 불가능합니다." value={form.username} size={22} /> diff --git a/src/containers/register/RegisterFormContainer.tsx b/src/containers/register/RegisterFormContainer.tsx index ddde94ab..2512701a 100644 --- a/src/containers/register/RegisterFormContainer.tsx +++ b/src/containers/register/RegisterFormContainer.tsx @@ -67,16 +67,16 @@ const RegisterFormContainer: React.FC = ({ // validate const validation = { displayName: (text: string) => { - if (text === '') { + if (text.trim() === '') { return '이름을 입력해주세요.'; } - if (text.length > 45) { + if (text.trim().length > 45) { return '이름은 최대 45자까지 입력 할 수 있습니다.'; } }, username: (text: string) => { if (!/^[a-z0-9-_]{3,16}$/.test(text)) { - return '아이디는 3~16자의 알파벳 소문자,숫자,혹은 - _ 으로 이루어져야 합니다.'; + return '사용자 ID는 3~16자의 알파벳 소문자,숫자,혹은 - _ 으로 이루어져야 합니다.'; } }, shortBio: (text: string) => { diff --git a/src/lib/api/apiClient.ts b/src/lib/api/apiClient.ts index 891fe291..3ca121e5 100644 --- a/src/lib/api/apiClient.ts +++ b/src/lib/api/apiClient.ts @@ -4,6 +4,7 @@ const host = process.env.NODE_ENV === 'development' ? '/' : process.env.REACT_APP_API_HOST || '/'; + const apiClient = axios.create({ baseURL: host, withCredentials: true, From f40d5155ce7f8a1445b3642e78c750d5a587a5a2 Mon Sep 17 00:00:00 2001 From: carrick Date: Wed, 28 Aug 2024 10:29:10 +0900 Subject: [PATCH 20/52] update: registerForm placeholders --- src/components/register/RegisterForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/register/RegisterForm.tsx b/src/components/register/RegisterForm.tsx index 44a9bb73..8fff7bf5 100644 --- a/src/components/register/RegisterForm.tsx +++ b/src/components/register/RegisterForm.tsx @@ -104,7 +104,7 @@ const RegisterForm: React.FC = ({ name="displayName" onChange={onChange} label="프로필 이름" - placeholder="프로필 이름을 입력하세요. 프로필 설정에서 변경이 가능합니다." + placeholder="프로필 이름을 입력하세요." value={form.displayName} size={20} /> @@ -123,7 +123,7 @@ const RegisterForm: React.FC = ({ name="username" onChange={onChange} label="사용자 ID" - placeholder="새 사용자 ID를 입력하세요. 변경이 불가능합니다." + placeholder="사용자 ID를 입력하세요." value={form.username} size={22} /> From a1e34cf9beb01432c42eade31c523fe5e7ff6d45 Mon Sep 17 00:00:00 2001 From: carrick Date: Tue, 3 Sep 2024 13:41:53 +0900 Subject: [PATCH 21/52] chore: Trim tag input value and limit to 255 characters --- src/components/write/TagInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/write/TagInput.tsx b/src/components/write/TagInput.tsx index 7739aa77..60176dbe 100644 --- a/src/components/write/TagInput.tsx +++ b/src/components/write/TagInput.tsx @@ -50,7 +50,7 @@ const TagInput: React.FC = ({ onChange, tags: initialTags }) => { setValue(''); if (tag === '' || tags.includes(tag)) return; let processed = tag; - processed = tag.trim(); + processed = tag.trim().slice(0,255); if (processed.indexOf(' #') > 0) { const tempTags: string[] = []; const regex = /#(\S+)/g; From 96c35cae9b3c85aba1defbff66e0ddcea3d5840d Mon Sep 17 00:00:00 2001 From: carrick Date: Tue, 3 Sep 2024 21:13:38 +0900 Subject: [PATCH 22/52] feat: Improve validation for RegisterForm inputs --- .../register/RegisterFormContainer.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/containers/register/RegisterFormContainer.tsx b/src/containers/register/RegisterFormContainer.tsx index 2512701a..f490c5ba 100644 --- a/src/containers/register/RegisterFormContainer.tsx +++ b/src/containers/register/RegisterFormContainer.tsx @@ -16,6 +16,7 @@ import { withRouter, RouteComponentProps } from 'react-router-dom'; import qs from 'qs'; import { useApolloClient } from '@apollo/react-hooks'; import { GET_CURRENT_USER } from '../../lib/graphql/user'; +import { isEmpty, trim } from 'ramda'; interface RegisterFormContainerProps extends RouteComponentProps<{}> {} @@ -65,16 +66,28 @@ const RegisterFormContainer: React.FC = ({ const onSubmit = async (form: RegisterFormType) => { setError(null); // validate + + const isCustomEmpty = (str: string) => { + if (typeof str !== 'string') { + return isEmpty(str); + } + return isEmpty(trim(str.replace(/\s/g, ''))); + }; + const validation = { displayName: (text: string) => { - if (text.trim() === '') { - return '이름을 입력해주세요.'; + if (isCustomEmpty(text)) { + return '프로필 이름을 입력해주세요.'; } if (text.trim().length > 45) { return '이름은 최대 45자까지 입력 할 수 있습니다.'; } }, username: (text: string) => { + if (isCustomEmpty(text)) { + return '사용자 ID를 입력해주세요.'; + } + if (!/^[a-z0-9-_]{3,16}$/.test(text)) { return '사용자 ID는 3~16자의 알파벳 소문자,숫자,혹은 - _ 으로 이루어져야 합니다.'; } From 35e0a1338d1c1b2502c7fbcd2c8657fe3a5468e8 Mon Sep 17 00:00:00 2001 From: carrick Date: Tue, 3 Sep 2024 21:26:21 +0900 Subject: [PATCH 23/52] feat: add check empty input --- src/components/write/TagInput.tsx | 6 +++--- src/containers/register/RegisterFormContainer.tsx | 12 +++--------- src/lib/utils.ts | 8 ++++++++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/write/TagInput.tsx b/src/components/write/TagInput.tsx index 60176dbe..0c470ed3 100644 --- a/src/components/write/TagInput.tsx +++ b/src/components/write/TagInput.tsx @@ -6,6 +6,7 @@ import transitions from '../../lib/styles/transitions'; import { mediaQuery } from '../../lib/styles/media'; import { useTransition, animated } from 'react-spring'; import OutsideClickHandler from 'react-outside-click-handler'; +import { isEmptyOrWhitespace } from '../../lib/utils'; export interface TagInputProps { ref?: React.RefObject; @@ -48,9 +49,8 @@ const TagInput: React.FC = ({ onChange, tags: initialTags }) => { (tag: string) => { ignore.current = true; setValue(''); - if (tag === '' || tags.includes(tag)) return; - let processed = tag; - processed = tag.trim().slice(0,255); + if (isEmptyOrWhitespace(tag) || tags.includes(tag)) return; + let processed = tag.trim().slice(0, 255); if (processed.indexOf(' #') > 0) { const tempTags: string[] = []; const regex = /#(\S+)/g; diff --git a/src/containers/register/RegisterFormContainer.tsx b/src/containers/register/RegisterFormContainer.tsx index f490c5ba..d58a3f3d 100644 --- a/src/containers/register/RegisterFormContainer.tsx +++ b/src/containers/register/RegisterFormContainer.tsx @@ -17,6 +17,7 @@ import qs from 'qs'; import { useApolloClient } from '@apollo/react-hooks'; import { GET_CURRENT_USER } from '../../lib/graphql/user'; import { isEmpty, trim } from 'ramda'; +import { isEmptyOrWhitespace } from '../../lib/utils'; interface RegisterFormContainerProps extends RouteComponentProps<{}> {} @@ -67,16 +68,9 @@ const RegisterFormContainer: React.FC = ({ setError(null); // validate - const isCustomEmpty = (str: string) => { - if (typeof str !== 'string') { - return isEmpty(str); - } - return isEmpty(trim(str.replace(/\s/g, ''))); - }; - const validation = { displayName: (text: string) => { - if (isCustomEmpty(text)) { + if (isEmptyOrWhitespace(text)) { return '프로필 이름을 입력해주세요.'; } if (text.trim().length > 45) { @@ -84,7 +78,7 @@ const RegisterFormContainer: React.FC = ({ } }, username: (text: string) => { - if (isCustomEmpty(text)) { + if (isEmptyOrWhitespace(text)) { return '사용자 ID를 입력해주세요.'; } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 4f46ab36..4f98462f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,7 @@ import distanceInWordsToNow from 'date-fns/formatDistanceToNow'; import format from 'date-fns/format'; import koLocale from 'date-fns/locale/ko'; +import { isEmpty, trim } from 'ramda'; export const formatDate = (date: string): string => { const d = new Date(date); @@ -115,3 +116,10 @@ export const createFallbackTitle = (username: string | null) => { }; export const ssrEnabled = process.env.REACT_APP_SSR === 'enabled'; + +export const isEmptyOrWhitespace = (str: string) => { + if (typeof str !== 'string') { + return isEmpty(str); + } + return isEmpty(trim(str.replace(/\s/g, ''))); +}; From 907a9fc8a63814a2650ade15182326a404479d0a Mon Sep 17 00:00:00 2001 From: Minjun Kim Date: Thu, 7 Nov 2024 16:25:47 +0900 Subject: [PATCH 24/52] Update policyData.ts --- src/components/policy/policyData.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/policy/policyData.ts b/src/components/policy/policyData.ts index f0ab43db..a6a2d673 100644 --- a/src/components/policy/policyData.ts +++ b/src/components/policy/policyData.ts @@ -117,6 +117,7 @@ const data = { 1. "회원"의 게시물이 "정보통신망법" 및 "저작권법"등 관련법에 위반되는 내용을 포함하는 경우, 권리자는 관련법이 정한 절차에 따라 해당 게시물의 게시중단 및 삭제 등을 요청할 수 있으며, "회사"는 관련법에 따라 조치를 취하여야 합니다. 2. "회사"는 전항에 따른 권리자의 요청이 없는 경우라도 권리침해가 인정될 만한 사유가 있거나 기타 회사 정책 및 관련법에 위반되는 경우에는 관련법에 따라 해당 게시물에 대해 임시조치 등을 취할 수 있습니다. 3. 본 조에 따른 세부절차는 "정보통신망법" 및 "저작권법"이 규정한 범위 내에서 회사가 정한 게시중단요청서비스에 따릅니다. +4. "회사"는 [커뮤니티 가이드라인](https://chafgames.notion.site/137fd2be2cdc8046b0f1fb8c93ac26d1?pvs=74)에 따라 게시물의 게시중단 및 삭제처리를 할 수 있습니다. ## 제 10조 권리의 귀속 From 52e470b9b0177394e6e933a5fcdb2f9714c6c93a Mon Sep 17 00:00:00 2001 From: velopert Date: Mon, 18 Nov 2024 03:48:19 +0900 Subject: [PATCH 25/52] fix: adds job position --- src/components/post/JobPositions.tsx | 160 +++++++++++++++++++++++++++ src/containers/post/PostViewer.tsx | 44 +++++++- src/lib/graphql/ad.ts | 22 ++++ 3 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 src/components/post/JobPositions.tsx diff --git a/src/components/post/JobPositions.tsx b/src/components/post/JobPositions.tsx new file mode 100644 index 00000000..a0471e66 --- /dev/null +++ b/src/components/post/JobPositions.tsx @@ -0,0 +1,160 @@ +import { useQuery } from '@apollo/react-hooks'; +import React, { useEffect, useRef, useState } from 'react'; +import { JOB_POSITIONS, JobPosition } from '../../lib/graphql/ad'; +import styled from 'styled-components'; +import VelogResponsive from '../velog/VelogResponsive'; +import Typography from '../common/Typography'; +import { themedPalette } from '../../lib/styles/themes'; +import { ellipsis } from '../../lib/styles/utils'; +import media from '../../lib/styles/media'; +import gtag from '../../lib/gtag'; + +type Props = { + category: 'frontend' | 'backend' | 'mobile' | 'python' | 'node' | 'ai' | null; +}; + +function JobPositions({ category }: Props) { + const [isObserved, setIsObserved] = useState(false); + const { data } = useQuery<{ jobPositions: JobPosition[] }>(JOB_POSITIONS, { + variables: { + category: category ?? undefined, + }, + skip: !isObserved, + }); + + const ref = useRef(null); + const initializedRef = useRef(false); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting && !initializedRef.current) { + setIsObserved(true); + } + }); + }, + { + rootMargin: '300px', + threshold: 0, + }, + ); + if (!ref.current) return; + observer.observe(ref.current); + return () => { + observer.disconnect(); + }; + }, []); + + const onClick = () => { + gtag('event', 'job_position_click'); + }; + + useEffect(() => { + if (!isObserved) { + return; + } + gtag('event', 'job_position_view'); + }, [isObserved]); + + if (!data?.jobPositions) + return ( + +
+
+ ); + + return ( + +
+ +

관련 채용 정보

+ + {data.jobPositions.map((jobPosition) => ( + +
+ + + + + + + + + {jobPosition.name} + + ))} + + + + ); +} + +const Block = styled(VelogResponsive)` + ${media.small} { + h4 { + padding-left: 1rem; + padding-right: 1rem; + } + } +`; +const Container = styled.div` + display: flex; + gap: 1rem; + a { + display: block; + color: inherit; + &:hover { + text-decoration: none; + color: inherit; + } + } + ${media.small} { + padding-left: 0.5rem; + padding-right: 0.5rem; + gap: 0.5rem; + overflow-x: auto; + overflow-y: hidden; + padding-bottom: 1rem; + } +`; + +const Card = styled.div` + width: 25%; + ${media.small} { + flex-shrink: 0; + width: 27vw; + } +`; + +const Thumbnail = styled.img` + width: 100%; + aspect-ratio: 400 / 292; + object-fit: cover; + border-radius: 4px; +`; + +const Company = styled.div` + display: flex; + gap: 0.5rem; + img { + display: block; + width: 16px; + height: 16px; + } + font-size: 10px; + align-items: center; + color: ${themedPalette.text2}; + ${ellipsis}; + margin-bottom: 0.5rem; +`; + +const JobTitle = styled.a` + font-size: 12px; + font-weight: 600; + line-height: 1.25; +`; + +export default JobPositions; diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 7ce37736..9b5d75e7 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -46,6 +46,7 @@ import gtag from '../../lib/gtag'; import FollowButton from '../../components/common/FollowButton'; import { BANNER_ADS } from '../../lib/graphql/ad'; import PostBanner from '../../components/post/PostBanner'; +import JobPositions from '../../components/post/JobPositions'; const UserProfileWrapper = styled(VelogResponsive)` margin-top: 16rem; @@ -75,6 +76,7 @@ const PostViewer: React.FC = ({ }) => { const setShowFooter = useSetShowFooter(); const [showRecommends, setShowRecommends] = useState(false); + useEffect(() => { window.scrollTo(0, 0); }, [username, urlSlug]); @@ -251,6 +253,40 @@ const PostViewer: React.FC = ({ } }, [customAd, shouldShowBanner, shouldShowFooterBanner]); + const category = useMemo(() => { + const frontendKeywords = ['프런트엔드', '리액트', 'vue', 'react', 'next']; + const backendKeywords = ['백엔드', '서버', '데이터베이스', 'db']; + const aiKeywords = ['인공지능', '머신러닝', '딥러닝', 'ai']; + const mobileKeywords = [ + '모바일', + '안드로이드', + 'ios', + 'react native', + '플러터', + 'flutter', + ]; + const pythonKeywords = ['파이썬', 'python']; + const nodeKeywords = ['노드', 'node', 'express', 'koa', 'nest']; + + if (!data?.post) return null; + const { post } = data; + const merged = post.title + .concat(post.tags.join(',')) + .concat(post.body) + .toLowerCase(); + if (frontendKeywords.some((keyword) => merged.includes(keyword))) + return 'frontend'; + if (backendKeywords.some((keyword) => merged.includes(keyword))) + return 'backend'; + if (aiKeywords.some((keyword) => merged.includes(keyword))) return 'ai'; + if (mobileKeywords.some((keyword) => merged.includes(keyword))) + return 'mobile'; + if (pythonKeywords.some((keyword) => merged.includes(keyword))) + return 'python'; + if (nodeKeywords.some((keyword) => merged.includes(keyword))) return 'node'; + return null; + }, [data]); + const onRemove = async () => { if (!data || !data.post) return; setIsRemoveLoading(true); @@ -486,10 +522,10 @@ const PostViewer: React.FC = ({ /> - {shouldShowBanner && isContentLongEnough ? ( + {shouldShowBanner && isContentLongEnough && customAd ? ( ) : null} - {shouldShowFooterBanner ? ( + {shouldShowFooterBanner && customAd ? ( ) : null} = ({ postId={post.id} ownPost={post.user.id === userId} /> + {(shouldShowBanner || shouldShowFooterBanner) && !customAd ? ( + + ) : null} + {showRecommends ? ( ) : null} diff --git a/src/lib/graphql/ad.ts b/src/lib/graphql/ad.ts index 483be812..63c39656 100644 --- a/src/lib/graphql/ad.ts +++ b/src/lib/graphql/ad.ts @@ -8,6 +8,15 @@ export type Ad = { url: string; }; +export type JobPosition = { + id: string; + name: string; + companyName: string; + companyLogo: string; + thumbnail: string; + url: string; +}; + export const BANNER_ADS = gql` query BannerAds($writerUsername: String!) { bannerAds(writer_username: $writerUsername) { @@ -19,3 +28,16 @@ export const BANNER_ADS = gql` } } `; + +export const JOB_POSITIONS = gql` + query JobPositions($category: String) { + jobPositions(category: $category) { + id + name + companyName + companyLogo + thumbnail + url + } + } +`; From f8bda2850c60e3e375eb35ada620aca51d0c6c57 Mon Sep 17 00:00:00 2001 From: Kwonkyu Date: Fri, 27 Dec 2024 22:17:25 +0900 Subject: [PATCH 26/52] =?UTF-8?q?=ED=83=9C=EA=B7=B8=EA=B0=80=20=EB=B9=84?= =?UTF-8?q?=EC=96=B4=EC=9E=88=EC=9D=84=20=EA=B2=BD=EC=9A=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=EB=A5=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/write/TagInput.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/write/TagInput.tsx b/src/components/write/TagInput.tsx index 0c470ed3..a4b11b69 100644 --- a/src/components/write/TagInput.tsx +++ b/src/components/write/TagInput.tsx @@ -28,7 +28,6 @@ const TagInput: React.FC = ({ onChange, tags: initialTags }) => { const ignore = useRef(false); useEffect(() => { - if (tags.length === 0) return; onChange(tags); }, [tags, onChange]); From 588a328437cd9061068cdb1f1dcba4fccc455c33 Mon Sep 17 00:00:00 2001 From: carrick Date: Mon, 6 Jan 2025 08:34:26 +0900 Subject: [PATCH 27/52] chore: fix git ignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index b91ca42f..0516f767 100755 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,7 @@ jspm_packages # Serverless directories .serverless .webpack + +# ignore setting +.idea +.vscode From a600503429b9c69772386b5736b1af47b1ec68bb Mon Sep 17 00:00:00 2001 From: velopert Date: Wed, 5 Feb 2025 18:56:48 +0900 Subject: [PATCH 28/52] fix: job positions logic --- src/components/post/JobPositions.tsx | 34 +++++++++++++++++--------- src/containers/post/PostViewer.tsx | 36 +++++++++++++++++++++------- src/lib/api/jobs.ts | 21 ++++++++++++++++ 3 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 src/lib/api/jobs.ts diff --git a/src/components/post/JobPositions.tsx b/src/components/post/JobPositions.tsx index a0471e66..b6869448 100644 --- a/src/components/post/JobPositions.tsx +++ b/src/components/post/JobPositions.tsx @@ -8,6 +8,7 @@ import { themedPalette } from '../../lib/styles/themes'; import { ellipsis } from '../../lib/styles/utils'; import media from '../../lib/styles/media'; import gtag from '../../lib/gtag'; +import { getJobs, Job } from '../../lib/api/jobs'; type Props = { category: 'frontend' | 'backend' | 'mobile' | 'python' | 'node' | 'ai' | null; @@ -15,16 +16,19 @@ type Props = { function JobPositions({ category }: Props) { const [isObserved, setIsObserved] = useState(false); - const { data } = useQuery<{ jobPositions: JobPosition[] }>(JOB_POSITIONS, { - variables: { - category: category ?? undefined, - }, - skip: !isObserved, - }); + const [data, setData] = useState([]); const ref = useRef(null); const initializedRef = useRef(false); + useEffect(() => { + getJobs(category || 'general').then((jobs) => { + const shuffled = jobs.sort(() => Math.random() - 0.5); + const sliced = shuffled.slice(0, 3); + setData(sliced); + }); + }, [category]); + useEffect(() => { const observer = new IntersectionObserver( (entries) => { @@ -57,7 +61,7 @@ function JobPositions({ category }: Props) { gtag('event', 'job_position_view'); }, [isObserved]); - if (!data?.jobPositions) + if (!data) return (
@@ -70,7 +74,7 @@ function JobPositions({ category }: Props) {

관련 채용 정보

- {data.jobPositions.map((jobPosition) => ( + {data.map((jobPosition) => ( @@ -84,6 +88,7 @@ function JobPositions({ category }: Props) {
{jobPosition.name} + {jobPosition.summary} ))} @@ -122,10 +127,10 @@ const Container = styled.div` `; const Card = styled.div` - width: 25%; + width: 33.33%; ${media.small} { flex-shrink: 0; - width: 27vw; + width: 60vw; } `; @@ -152,9 +157,16 @@ const Company = styled.div` `; const JobTitle = styled.a` - font-size: 12px; + font-size: 14px; font-weight: 600; line-height: 1.25; `; +const JobDescription = styled.div` + margin-top: 8px; + color: ${themedPalette.text2}; + font-size: 12px; + line-height: 1.5; +`; + export default JobPositions; diff --git a/src/containers/post/PostViewer.tsx b/src/containers/post/PostViewer.tsx index 9b5d75e7..34a1e936 100644 --- a/src/containers/post/PostViewer.tsx +++ b/src/containers/post/PostViewer.tsx @@ -223,7 +223,7 @@ const PostViewer: React.FC = ({ const isOwnPost = post.user.id === userId; const isVeryOld = Date.now() - new Date(post.released_at).getTime() > - 1000 * 60 * 60 * 24 * 30; + 1000 * 60 * 60 * 24 * 10; if (isOwnPost) return false; if (!isVeryOld) return false; @@ -254,16 +254,24 @@ const PostViewer: React.FC = ({ }, [customAd, shouldShowBanner, shouldShowFooterBanner]); const category = useMemo(() => { - const frontendKeywords = ['프런트엔드', '리액트', 'vue', 'react', 'next']; + const frontendKeywords = [ + '프런트엔드', + '리액트', + 'vue', + 'react', + 'next', + '프론트엔드', + ]; const backendKeywords = ['백엔드', '서버', '데이터베이스', 'db']; - const aiKeywords = ['인공지능', '머신러닝', '딥러닝', 'ai']; + const aiKeywords = ['인공지능', '머신러닝', '딥러닝', 'nlp', 'llm']; const mobileKeywords = [ - '모바일', '안드로이드', 'ios', 'react native', '플러터', 'flutter', + 'swift', + 'xcode', ]; const pythonKeywords = ['파이썬', 'python']; const nodeKeywords = ['노드', 'node', 'express', 'koa', 'nest']; @@ -274,15 +282,25 @@ const PostViewer: React.FC = ({ .concat(post.tags.join(',')) .concat(post.body) .toLowerCase(); + if ( + aiKeywords.some((keyword) => { + const value = merged.includes(keyword); + if (value) { + console.log(merged); + console.log(keyword); + } + return value; + }) + ) + return 'ai'; if (frontendKeywords.some((keyword) => merged.includes(keyword))) return 'frontend'; - if (backendKeywords.some((keyword) => merged.includes(keyword))) - return 'backend'; - if (aiKeywords.some((keyword) => merged.includes(keyword))) return 'ai'; if (mobileKeywords.some((keyword) => merged.includes(keyword))) return 'mobile'; if (pythonKeywords.some((keyword) => merged.includes(keyword))) return 'python'; + if (backendKeywords.some((keyword) => merged.includes(keyword))) + return 'backend'; if (nodeKeywords.some((keyword) => merged.includes(keyword))) return 'node'; return null; }, [data]); @@ -522,10 +540,10 @@ const PostViewer: React.FC = ({ /> - {shouldShowBanner && isContentLongEnough && customAd ? ( + {shouldShowBanner && isContentLongEnough ? ( ) : null} - {shouldShowFooterBanner && customAd ? ( + {shouldShowFooterBanner ? ( ) : null} (`/jobs/${category}`); + return response.data; +} + +export type Job = { + id: number; + name: string; + companyName: string; + companyLogo: string; + thumbnail: string; + url: string; + jobId: number; + summary: string; +}; From aa046291a0fb7aebf6f3c07fee61372008cb9c21 Mon Sep 17 00:00:00 2001 From: velopert Date: Wed, 5 Feb 2025 19:11:59 +0900 Subject: [PATCH 29/52] fix: link inside description --- src/components/post/JobPositions.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/post/JobPositions.tsx b/src/components/post/JobPositions.tsx index b6869448..d9e88808 100644 --- a/src/components/post/JobPositions.tsx +++ b/src/components/post/JobPositions.tsx @@ -88,7 +88,9 @@ function JobPositions({ category }: Props) { {jobPosition.name} - {jobPosition.summary} + + {jobPosition.summary} + ))} From bf48305f32cdc43f51f80a91e801fef2ef1f5d8f Mon Sep 17 00:00:00 2001 From: velopert Date: Thu, 8 May 2025 11:10:31 +0900 Subject: [PATCH 30/52] fix: update identifier --- src/server/Html.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/Html.tsx b/src/server/Html.tsx index 5fa9641b..52b25f69 100644 --- a/src/server/Html.tsx +++ b/src/server/Html.tsx @@ -74,7 +74,7 @@ function Html({ > */}