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
8 changes: 6 additions & 2 deletions apps/client/src/pages/jobPins/JobPins.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useGetJobPinsArticles } from '@pages/jobPins/apis/queries';
import JobPinsBottomNotice from '@pages/jobPins/components/JobPinsBottomNotice';
import { useJobPinsBottomNotice } from '@pages/jobPins/hooks/useJobPinsBottomNotice';
import Footer from '@pages/myBookmark/components/footer/Footer';
import { Card } from '@pinback/design-system/ui';
import { useInfiniteScroll } from '@shared/hooks/useInfiniteScroll';
import { useRef } from 'react';

const JobPins = () => {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const { scrollContainerRef, isBottomNoticeVisible, handleBottomWheel } =
useJobPinsBottomNotice();

const { data, isPending, fetchNextPage, hasNextPage } =
useGetJobPinsArticles();
Expand Down Expand Up @@ -43,6 +45,7 @@ const JobPins = () => {
) : articlesToDisplay.length > 0 ? (
<div
ref={scrollContainerRef}
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) => (
Expand All @@ -60,6 +63,7 @@ const JobPins = () => {
/>
))}

<JobPinsBottomNotice visible={isBottomNoticeVisible} />
<div ref={observerRef} style={{ height: '1px', width: '100%' }} />
Comment on lines +66 to 67
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "[1] useInfiniteScroll 구현 파일 탐색"
fd 'useInfiniteScroll\.(ts|tsx|js|jsx)$' || echo "No useInfiniteScroll hook file found by exact name"

echo
echo "[2] InfiniteScroll 관련 파일 탐색"
fd -t f -e ts -e tsx -e js -e jsx | xargs rg -l 'useInfiniteScroll|InfiniteScroll' | head -20

echo
echo "[3] JobPins 파일 내용 확인"
cat -n apps/client/src/pages/jobPins/JobPins.tsx | head -100

echo
echo "[4] JobPins 전체 구조"
wc -l apps/client/src/pages/jobPins/JobPins.tsx

Repository: Pinback-Team/pinback-client

Length of output: 3697


🏁 Script executed:

echo "[5] 디렉터리 구조 확인"
find apps/client/src -type f -name "*.ts" -o -name "*.tsx" | grep -i "infinite\|scroll\|hook" | head -20

Repository: Pinback-Team/pinback-client

Length of output: 676


🏁 Script executed:

echo "[6] useInfiniteScroll 또는 관련 훅 검색"
rg -t ts -t tsx 'export.*useInfiniteScroll|function useInfiniteScroll|const useInfiniteScroll' apps/

Repository: Pinback-Team/pinback-client

Length of output: 148


🏁 Script executed:

echo "[1] useInfiniteScroll 구현 확인"
cat -n apps/client/src/shared/hooks/useInfiniteScroll.ts

Repository: Pinback-Team/pinback-client

Length of output: 1509


🏁 Script executed:

echo "[2] JobPinsBottomNotice 컴포넌트 확인"
cat -n apps/client/src/pages/jobPins/components/JobPinsBottomNotice.tsx

Repository: Pinback-Team/pinback-client

Length of output: 1005


🏁 Script executed:

echo "[3] useJobPinsBottomNotice 훅 확인"
cat -n apps/client/src/pages/jobPins/hooks/useJobPinsBottomNotice.ts

Repository: Pinback-Team/pinback-client

Length of output: 1948


🏁 Script executed:

echo "[4] useGetJobPinsArticles 쿼리 함수 확인"
fd 'useGetJobPinsArticles' apps/ --exec cat -n {} \;

Repository: Pinback-Team/pinback-client

Length of output: 105


공지 높이 애니메이션으로 인한 센티넬 재교차 가능성이 있습니다.

Line 66-67에서 JobPinsBottomNotice가 300ms 전환 애니메이션으로 높이가 변하면(max-h: 0 ↔ 10rem), 센티넬이 재진입할 수 있습니다. 현재 useInfiniteScrollhasNextPage 가드만 사용하며, 옵저버 콜백 내부에 isFetching 체크가 없어 진행 중인 페치 중 레이아웃 변동 시 중복 호출 위험이 있습니다.

개선안: useInfiniteScroll 콜백에서 다음과 같이 추가 가드를 적용하세요.

if (entry.isIntersecting && hasNextPage) {
  fetchNextPage();
}

또는 TanStack Query의 isFetchingNextPage 상태를 props로 받아 콜백 내에서 체크하면 더욱 견고합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/jobPins/JobPins.tsx` around lines 66 - 67, The sentinel
re-enters while JobPinsBottomNotice animates height (see JobPinsBottomNotice and
the div using observerRef), causing duplicate fetches because useInfiniteScroll
only checks hasNextPage; update the intersection callback inside
useInfiniteScroll (or the place where fetchNextPage is called) to guard with
entry.isIntersecting and also check in-flight state (either isFetching or better
isFetchingNextPage passed in from TanStack Query) before calling fetchNextPage
so it only runs when intersecting and not already fetching.

</div>
) : (
Expand Down
26 changes: 26 additions & 0 deletions apps/client/src/pages/jobPins/components/JobPinsBottomNotice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import JobPinsBottomSpinner from './JobPinsBottomSpinner';

interface JobPinsBottomNoticeProps {
visible: boolean;
}

const JobPinsBottomNotice = ({ visible }: JobPinsBottomNoticeProps) => {
return (
<div
className={`w-full overflow-hidden transition-all duration-300 ease-out ${
visible
? 'max-h-[10rem] translate-y-0 py-[1.2rem] opacity-100'
: 'max-h-0 translate-y-2 py-0 opacity-0'
}`}
>
Comment on lines +9 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

숨김 상태를 접근성 트리에도 반영해 주세요.

Line 10-14는 시각적 접힘만 처리해서, 숨김 상태 문구가 보조기기에 노출될 수 있습니다. 래퍼에 aria-hidden={!visible}를 같이 두는 게 안전합니다.

제안 diff
     <div
+      aria-hidden={!visible}
       className={`w-full overflow-hidden transition-all duration-300 ease-out ${
         visible
           ? 'max-h-[10rem] translate-y-0 py-[1.2rem] opacity-100'
           : 'max-h-0 translate-y-2 py-0 opacity-0'
       }`}
     >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
className={`w-full overflow-hidden transition-all duration-300 ease-out ${
visible
? 'max-h-[10rem] translate-y-0 py-[1.2rem] opacity-100'
: 'max-h-0 translate-y-2 py-0 opacity-0'
}`}
>
<div
aria-hidden={!visible}
className={`w-full overflow-hidden transition-all duration-300 ease-out ${
visible
? 'max-h-[10rem] translate-y-0 py-[1.2rem] opacity-100'
: 'max-h-0 translate-y-2 py-0 opacity-0'
}`}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/jobPins/components/JobPinsBottomNotice.tsx` around
lines 9 - 15, The wrapper DIV that toggles visibility visually (the JSX block in
JobPinsBottomNotice where the className depends on `visible`) doesn't update
accessibility tree; add an `aria-hidden` attribute bound to the same `visible`
state (e.g., `aria-hidden={!visible}`) on that wrapper so assistive technologies
respect the hidden state, keeping the attribute logic consistent with the
existing `visible` conditional used in the className.

<div className="flex w-full flex-col items-center gap-[0.8rem]">
<p className="body2-m text-font-gray-3 text-center">
관심 직무 핀은 계속 업데이트 돼요!
</p>
<JobPinsBottomSpinner />
</div>
</div>
);
};

export default JobPinsBottomNotice;
42 changes: 42 additions & 0 deletions apps/client/src/pages/jobPins/components/JobPinsBottomSpinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useId } from 'react';

const JobPinsBottomSpinner = () => {
const gradientId = useId();

return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
className="animate-spin-smooth h-[3rem] w-[3rem]"
>
<circle cx="15" cy="15" r="13" stroke="#E5E7EB" strokeWidth="4" />
<circle
cx="15"
cy="15"
r="13"
stroke={`url(#${gradientId})`}
strokeWidth="4"
strokeDasharray="46 120"
strokeLinecap="round"
/>
<defs>
<radialGradient
id={gradientId}
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(13 9.75) rotate(122.975) scale(20.3637)"
>
<stop stopColor="var(--color-gradient-start, #cff080)" />
<stop offset="1" stopColor="var(--color-gradient-end, #33d08f)" />
</radialGradient>
</defs>
</svg>
);
};

export default JobPinsBottomSpinner;
59 changes: 59 additions & 0 deletions apps/client/src/pages/jobPins/hooks/useJobPinsBottomNotice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useCallback, useEffect, useRef, useState } from 'react';

export const useJobPinsBottomNotice = () => {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const hideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [isBottomNoticeVisible, setIsBottomNoticeVisible] = useState(false);

const showBottomNoticeTemporarily = useCallback(() => {
if (isBottomNoticeVisible) {
return;
}

setIsBottomNoticeVisible(true);
if (hideTimerRef.current) {
clearTimeout(hideTimerRef.current);
}

hideTimerRef.current = setTimeout(() => {
setIsBottomNoticeVisible(false);
hideTimerRef.current = null;
}, 2000);
}, [isBottomNoticeVisible]);
Comment on lines +8 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

공지 재노출 타이머가 갱신되지 않습니다.

Line 9의 조기 return 때문에, 공지가 보이는 동안 추가 휠 입력이 와도 숨김 타이머가 연장되지 않습니다. 연속 스크롤 시 공지가 너무 빨리 꺼질 수 있습니다.

제안 diff
   const showBottomNoticeTemporarily = useCallback(() => {
-    if (isBottomNoticeVisible) {
-      return;
-    }
-
     setIsBottomNoticeVisible(true);
     if (hideTimerRef.current) {
       clearTimeout(hideTimerRef.current);
     }

     hideTimerRef.current = setTimeout(() => {
       setIsBottomNoticeVisible(false);
       hideTimerRef.current = null;
     }, 2000);
-  }, [isBottomNoticeVisible]);
+  }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const showBottomNoticeTemporarily = useCallback(() => {
if (isBottomNoticeVisible) {
return;
}
setIsBottomNoticeVisible(true);
if (hideTimerRef.current) {
clearTimeout(hideTimerRef.current);
}
hideTimerRef.current = setTimeout(() => {
setIsBottomNoticeVisible(false);
hideTimerRef.current = null;
}, 2000);
}, [isBottomNoticeVisible]);
const showBottomNoticeTemporarily = useCallback(() => {
setIsBottomNoticeVisible(true);
if (hideTimerRef.current) {
clearTimeout(hideTimerRef.current);
}
hideTimerRef.current = setTimeout(() => {
setIsBottomNoticeVisible(false);
hideTimerRef.current = null;
}, 2000);
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/jobPins/hooks/useJobPinsBottomNotice.ts` around lines 8
- 22, The early return in showBottomNoticeTemporarily prevents updating the hide
timer while the notice is already visible, so additional wheel events won't
extend the display; remove the return and always reset/clear hideTimerRef and
set a new timeout, ensuring you still call setIsBottomNoticeVisible(true) when
appropriate; update the logic in showBottomNoticeTemporarily (referencing
isBottomNoticeVisible, hideTimerRef, setIsBottomNoticeVisible) to clear any
existing hideTimerRef, set visibility if not already true, and then assign a
fresh setTimeout to hideTimerRef so repeated calls extend the notice duration.


useEffect(() => {
return () => {
if (hideTimerRef.current) {
clearTimeout(hideTimerRef.current);
}
};
}, []);

const handleBottomWheel = useCallback(
(e: React.WheelEvent<HTMLDivElement>) => {
if (e.deltaY <= 0) {
return;
}

const container = scrollContainerRef.current;
if (!container) {
return;
}

const isAtBottom =
container.scrollTop + container.clientHeight >=
container.scrollHeight - 1;

if (isAtBottom) {
showBottomNoticeTemporarily();
}
},
[showBottomNoticeTemporarily]
);

return {
scrollContainerRef,
isBottomNoticeVisible,
handleBottomWheel,
};
};
Loading