-
Notifications
You must be signed in to change notification settings - Fork 1
Feat(Client): jobpin memo popup 구현 및 api 연결 #288
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
Changes from all commits
46a2546
c48ebae
5ef3c78
5c4dd8b
cc2fbc6
80f4f96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,9 @@ | ||
| import { useGetJobPinsArticles } from '@pages/jobPins/apis/queries'; | ||
| import { | ||
| useGetJobPinsArticleDetail, | ||
| useGetJobPinsArticles, | ||
| } from '@pages/jobPins/apis/queries'; | ||
| import JobPinsBottomNotice from '@pages/jobPins/components/JobPinsBottomNotice'; | ||
| import MemoPopup from '@pages/jobPins/components/MemoPopup'; | ||
| import { useJobPinsBottomNotice } from '@pages/jobPins/hooks/useJobPinsBottomNotice'; | ||
| import Footer from '@pages/myBookmark/components/footer/Footer'; | ||
| import { Card } from '@pinback/design-system/ui'; | ||
|
|
@@ -11,6 +15,11 @@ const JobPins = () => { | |
|
|
||
| const { data, isPending, fetchNextPage, hasNextPage } = | ||
| useGetJobPinsArticles(); | ||
| const { | ||
| mutate: getJobPinDetail, | ||
| data: jobPinDetail, | ||
| reset: resetJobPinDetail, | ||
| } = useGetJobPinsArticleDetail(); | ||
|
|
||
| const observerRef = useInfiniteScroll({ | ||
| fetchNextPage, | ||
|
|
@@ -33,6 +42,7 @@ const JobPins = () => { | |
| <p className="head3">관심 직무 핀</p> | ||
| {job && <p className="head3 text-main500">{job}</p>} | ||
| </div> | ||
|
|
||
| <p className="body3-r text-font-gray-3 mt-[0.8rem]"> | ||
| 같은 직무의 사람들이 저장한 아티클을 살펴봐요. 선택한 직무를 기준으로 | ||
| 최신 핀이 업데이트 돼요! | ||
|
|
@@ -48,32 +58,49 @@ const JobPins = () => { | |
| onWheel={handleBottomWheel} | ||
| className="scrollbar-hide mt-[2.6rem] flex h-screen flex-wrap content-start gap-[1.6rem] overflow-y-auto scroll-smooth" | ||
| > | ||
| {articlesToDisplay.map((article) => ( | ||
| <Card | ||
| key={article.articleId} | ||
| type="bookmark" | ||
| variant="save" | ||
| title={article.title} | ||
| imageUrl={article.thumbnailUrl} | ||
| content={article.memo} | ||
| category={article.category.categoryName} | ||
| categoryColor={article.category.categoryColor} | ||
| nickname={article.ownerName} | ||
| onClick={() => window.open(article.url, '_blank')} | ||
| /> | ||
| ))} | ||
| {articlesToDisplay.map((article) => { | ||
| const displayTitle = article.title?.trim() | ||
| ? article.title | ||
| : '제목 없음'; | ||
|
|
||
| const displayImageUrl = article.thumbnailUrl || undefined; | ||
|
|
||
| return ( | ||
| <Card | ||
| key={article.articleId} | ||
| type="bookmark" | ||
| variant="save" | ||
| title={displayTitle} | ||
| imageUrl={displayImageUrl} | ||
| content={article.memo} | ||
| category={article.category?.categoryName} | ||
| categoryColor={article.category?.categoryColor} | ||
| nickname={article.ownerName} | ||
| onClick={() => getJobPinDetail(article.articleId)} | ||
| /> | ||
| ); | ||
| })} | ||
|
|
||
| <JobPinsBottomNotice visible={isBottomNoticeVisible} /> | ||
| <div ref={observerRef} style={{ height: '1px', width: '100%' }} /> | ||
| </div> | ||
| ) : ( | ||
| // TODO: 아티클 없는경우 UI 수정 | ||
| <p className="body2-m text-font-gray-3 mt-[4rem]"> | ||
| 아직 공유된 아티클이 없어요. | ||
| </p> | ||
| )} | ||
|
|
||
| <Footer /> | ||
|
|
||
| {/* Memo Popup */} | ||
| {jobPinDetail && ( | ||
| <MemoPopup | ||
| userName={jobPinDetail.ownerName} | ||
| memo={jobPinDetail.memo} | ||
| onClose={resetJobPinDetail} | ||
| onGoArticle={() => window.open(jobPinDetail.url, '_blank')} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # 1. 지정된 파일의 line 101 확인
head -105 apps/client/src/pages/jobPins/JobPins.tsx | tail -10Repository: Pinback-Team/pinback-client Length of output: 337 🏁 Script executed: # 2. _blank 사용 시 noopener/noreferrer 누락 지점 확인
echo "=== All window.open with '_blank' ==="
rg -nP "window\.open\([^)]*['\"]_blank['\"]\s*\)" --type ts --type tsx
echo ""
echo "=== window.open with security parameters ==="
rg -nP "window\.open\([^)]*['\"]_blank['\"]\s*,\s*['\"][^'\"]*noopener[^'\"]*['\"]\s*\)" --type ts --type tsxRepository: Pinback-Team/pinback-client Length of output: 227 🏁 Script executed: # 3. 파일 존재 여부 확인
test -f apps/client/src/pages/jobPins/JobPins.tsx && echo "파일 존재함" || echo "파일 없음"Repository: Pinback-Team/pinback-client Length of output: 77 🏁 Script executed: # window.open() 사용 패턴 전체 검색 (파일 타입 필터 제거)
rg "window\.open" --no-type-list -A 1Repository: Pinback-Team/pinback-client Length of output: 514 🏁 Script executed: # 보안 파라미터 없이 '_blank' 사용하는 인스턴스 확인
rg "window\.open\([^)]*_blank[^)]*\)" --no-type-listRepository: Pinback-Team/pinback-client Length of output: 514 🏁 Script executed: # window.open 사용 패턴 전체 검색
rg "window\.open" -A 1Repository: Pinback-Team/pinback-client Length of output: 650 🏁 Script executed: # '_blank' 사용 시 보안 파라미터 여부 확인
rg "window\.open.*_blank"Repository: Pinback-Team/pinback-client Length of output: 410 새 탭 오픈 시 보안 파라미터
권장 수정안- onGoArticle={() => window.open(jobPinDetail.url, '_blank')}
+ onGoArticle={() =>
+ window.open(jobPinDetail.url, '_blank', 'noopener,noreferrer')
+ }동일한 패턴의 취약점이 다른 파일에도 존재합니다:
모든 인스턴스에 보안 파라미터를 추가해야 합니다. 🤖 Prompt for AI Agents |
||
| /> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,10 @@ | ||
| import { useInfiniteQuery } from '@tanstack/react-query'; | ||
| import { JobPinsResponse } from '@pages/jobPins/types/api'; | ||
| import { getJobPinsArticles } from './axios'; | ||
| import { useInfiniteQuery, useMutation } from '@tanstack/react-query'; | ||
| import { | ||
| getJobPinsArticleDetail, | ||
| getJobPinsArticles, | ||
| JobPinsDetailResponse, | ||
| } from './axios'; | ||
|
|
||
| export const useGetJobPinsArticles = () => { | ||
| return useInfiniteQuery<JobPinsResponse>({ | ||
|
|
@@ -16,3 +20,9 @@ export const useGetJobPinsArticles = () => { | |
| }, | ||
| }); | ||
| }; | ||
|
|
||
| export const useGetJobPinsArticleDetail = () => { | ||
| return useMutation<JobPinsDetailResponse, Error, number>({ | ||
| mutationFn: (articleId: number) => getJobPinsArticleDetail(articleId), | ||
| }); | ||
| }; | ||
|
Comment on lines
+24
to
+28
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 지금 막 생각을 해보니까 물론 직무 아티클이 사용자가 바꾸면 변동이 있지만 메타데이터인 만큼 사실 변동이 자주 있는 것은 아닌 것 같아서, 캐시를 못 쓰고 이를 클릭할 때마다 데이터를 새롭게 받아오면 그만큼 로딩이 느려지는 UX 문제가 있을 것 같아요.
만약 이 로딩이 꽤 느리게 느껴진다면 캐시는 하되 열 때/포커스 때 최신화는 강하게 설정해서 useQuery를 이용할 수 있을 것 같아요. queryKey에 id도 추가하고, [예시 코드] const { data, refetch } = useQuery({
queryKey: ['searchResult'],
queryFn: fetchSearch,
enabled: false, // 처음에는 자동으로 실행되지 않음!
});
// 버튼 클릭 시 호출
const handleClick = () => {
refetch(); // 이때 데이터를 가져오고, 결과는 캐싱
};이 부분도 다시 한번 생각해보시고 의견 내주시면 좋을 것 같아요~~ |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { Icon } from '@pinback/design-system/icons'; | ||
| import { Button } from '@pinback/design-system/ui'; | ||
|
|
||
| interface MemoPopupProps { | ||
| userName: string; | ||
| memo?: string | null; | ||
| onClose: () => void; | ||
| onGoArticle: () => void; | ||
| } | ||
|
|
||
| export default function MemoPopup({ | ||
| userName, | ||
| memo, | ||
| onClose, | ||
| onGoArticle, | ||
| }: MemoPopupProps) { | ||
| const hasMemo = memo && memo.trim().length > 0; | ||
|
|
||
| return ( | ||
| <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60"> | ||
| <div className="w-[31.2rem] rounded-[1.2rem] bg-white px-[3.2rem] py-[2.4rem]"> | ||
| {/* Header */} | ||
| <div className="flex items-center justify-between"> | ||
| <Icon name="logo" width={72} height={20} /> | ||
| <button onClick={onClose}> | ||
| <Icon name="ic_close" size={20} /> | ||
| </button> | ||
| </div> | ||
|
|
||
| {/* Title */} | ||
| <div className="caption1-sb text-font-black-1 mt-[1.6rem]"> | ||
| <span className="text-main500">{userName}</span>님의 메모 | ||
| </div> | ||
|
|
||
| {/* Memo */} | ||
| <div className="body3-r text-font-gray-3 mt-[1.2rem] h-[9.6rem] overflow-y-auto whitespace-pre-line"> | ||
| {hasMemo ? ( | ||
| memo | ||
| ) : ( | ||
| <div className="text-gray-400">작성된 메모가 없어요.</div> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Button */} | ||
| <Button onClick={onGoArticle} className="mt-[2.4rem] w-full"> | ||
| 아티클 보러 가기 | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기서 이렇게 reset을 사용해주셨군요! 👍