Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
3231a6f
fix: [#298] divider bg 색상 개인적용
Chasyuss Aug 15, 2025
77ca42c
fix: [#298] 직업상세 채용정보 위치 변경 및 ui 수정
Chasyuss Aug 15, 2025
51942e1
feat: [#298] 직업상세 tab 구현
Chasyuss Aug 18, 2025
5ce8179
fix: [#298] 직업 상세 Ui 변경 수정
Chasyuss Aug 18, 2025
26f434f
fix: [#299] husky에서 legacy는 체킹하지 않도록 로직 수정
cywin1018 Aug 15, 2025
8ddd238
chore: [#299] 레거시 컴포넌트
cywin1018 Aug 16, 2025
6db5199
refactor: [#299] Banner.tsx에 있는 태그 컴포넌트를 공통 컴포넌트 Tag.tsx로 리팩토링
cywin1018 Aug 16, 2025
aa75ae4
refactor: [#299] Tag 컴포넌트를 좀 더 유연하게 수정
cywin1018 Aug 16, 2025
20ed63b
feat: [#299] Slider.tsx 컴포넌트 제작
cywin1018 Aug 16, 2025
1ca5136
feat: [#299] 슬라이더와, 슬라이더를 보여주는 컨테이너 추가 및, 슬라이더 하단부가 흐릿하게 보이도록 mask cs…
cywin1018 Aug 24, 2025
741705a
feat: [#299] 홈의 로그인배너 부분 주석처리했던 부분 삭제 및 v2에 맞도록 ui 수정
cywin1018 Aug 24, 2025
90e583d
feat: [#303] 커뮤니티 오른쪽 사이드 구현
Chasyuss Aug 24, 2025
389a7ba
feat: [#310] 나의 투두의 추가하기 아이콘추가
cywin1018 Aug 24, 2025
1cc5147
feat: [#310] 체크리스트 ui 수정
cywin1018 Aug 24, 2025
468a8ce
Merge branch 'develop' of https://github.com/TEAM-DoDream/DoDream_FE …
Chasyuss Aug 25, 2025
66160db
fix: [#303] 커뮤니티 헤더 추가
Chasyuss Aug 20, 2025
c6bcc5d
feat: [#303] 할일 커뮤니티 구현
Chasyuss Aug 20, 2025
8adbe1b
chore: [#303] gray-50 컬러 추가
Chasyuss Aug 20, 2025
a65ea8a
feat: [#303] 커뮤니티 왼쪽 바 구현
Chasyuss Aug 22, 2025
818aacf
Merge pull request #314 from TEAM-DoDream/feature/home-299
cywin1018 Aug 27, 2025
846e600
fix: [#299] husky에서 legacy는 체킹하지 않도록 로직 수정
cywin1018 Aug 15, 2025
b8636e1
chore: [#299] 레거시 컴포넌트
cywin1018 Aug 16, 2025
dfc4d90
refactor: [#299] Banner.tsx에 있는 태그 컴포넌트를 공통 컴포넌트 Tag.tsx로 리팩토링
cywin1018 Aug 16, 2025
0bf7309
refactor: [#299] Tag 컴포넌트를 좀 더 유연하게 수정
cywin1018 Aug 16, 2025
0816e8f
feat: [#299] Slider.tsx 컴포넌트 제작
cywin1018 Aug 16, 2025
6083557
feat: [#299] 슬라이더와, 슬라이더를 보여주는 컨테이너 추가 및, 슬라이더 하단부가 흐릿하게 보이도록 mask cs…
cywin1018 Aug 24, 2025
a7b6014
feat: [#299] 홈의 로그인배너 부분 주석처리했던 부분 삭제 및 v2에 맞도록 ui 수정
cywin1018 Aug 24, 2025
0655de2
Merge pull request #313 from TEAM-DoDream/feature/mytodo-310
cywin1018 Aug 27, 2025
d443399
Merge pull request #315 from TEAM-DoDream/fix/jobdetailui-298
Chasyuss Aug 31, 2025
8f29d09
Merge pull request #316 from TEAM-DoDream/feature/community-303
Chasyuss Aug 31, 2025
58bef9e
feat: [#300] 인기 할일 조회를 위한 usePopularQuery 훅 및 검증 스키마 추가17
cywin1018 Aug 31, 2025
dfc3cdd
feat: 슬라이더 컴포넌트의 스타일 수정 및 인기 할일 데이터를 사용하여 슬라이더 데이터 동적 생성
cywin1018 Aug 31, 2025
d66eba3
fix: [#298] lint 에러 수정
Chasyuss Aug 31, 2025
183fde1
chore: [#298] jobinfo 헤더 연결
Chasyuss Aug 31, 2025
7eede88
feat: [#298] 직업상세 카테고리 연결
Chasyuss Aug 31, 2025
19532f1
[✨FEATURE] Amplitude 이벤트 추적 기능 추가 및 관련 코드 수정
cywin1018 Aug 31, 2025
148ff0d
[✨FEATURE] Amplitude 이벤트 추적 조건 개선 및 안전성 강화
cywin1018 Aug 31, 2025
e304657
[🔧FIX] pre-commit 스크립트에서 yarn 대신 npx lint-staged 사용으로 변경
cywin1018 Aug 31, 2025
acc82ca
feat: [#298] 내 할일 추가 연결
Chasyuss Sep 1, 2025
76d83ca
Merge pull request #319 from TEAM-DoDream/feat/amiplitude-318
cywin1018 Sep 1, 2025
e6ff5d5
fix: [#298] 충돌해결
Chasyuss Sep 1, 2025
10ff0f2
fix: [#298] 확장자 수정
Chasyuss Sep 1, 2025
b711b81
Merge pull request #320 from TEAM-DoDream/fix/jobdetailui-298
Chasyuss Sep 3, 2025
3d44700
Merge pull request #322 from TEAM-DoDream/feat/home_api-317
cywin1018 Sep 3, 2025
664c05c
feat:[#321] 홈 화면에서 사용자 정보를 가져오는 기능 추가 및 로그인 배너에 사용자 단계 표시 기능 구현
cywin1018 Sep 3, 2025
782ea92
Merge pull request #323 from TEAM-DoDream/feature/home-api-321
cywin1018 Sep 3, 2025
e80b6dd
Merge branch 'develop' of https://github.com/TEAM-DoDream/DoDream_FE …
Chasyuss Sep 4, 2025
10c5250
feat: [#325] 플로팅 모달 인기글 로그인/비로그인 연결
Chasyuss Sep 4, 2025
5fabd17
Merge pull request #326 from TEAM-DoDream/feature/floatinghot-325
Chasyuss Sep 4, 2025
caf32cb
feat: [#324] 커뮤니티 데이터 조회를 위한 커스텀 훅 및 드롭다운 컴포넌트 추가
cywin1018 Sep 4, 2025
f2a15e5
feat: [#324] 인기 할 일 조회를 위한 커스텀 훅 및 관련 컴포넌트 추가
cywin1018 Sep 4, 2025
c6a4c53
feat: [#303] 커뮤니티 최신순 모달 ui 구현
Chasyuss Sep 4, 2025
241c526
feat: [#324] 커뮤니티 드롭다운 및 상태 관리 개선 - 직업 목록을 객체 배열로 변경하고 선택된 직업의 ID를 관리…
cywin1018 Sep 4, 2025
03eadb5
Merge pull request #327 from TEAM-DoDream/feature/api-324
cywin1018 Sep 4, 2025
cca7d76
Merge branch 'develop' of https://github.com/TEAM-DoDream/DoDream_FE …
Chasyuss Sep 4, 2025
c59cdba
feat: [#303] 커뮤니티 연결
Chasyuss Sep 5, 2025
a5bb523
feat: [#303] 커뮤니티 내 할일 추가/취소 연결
Chasyuss Sep 5, 2025
5adb27c
fix: [#303] 커뮤니티 왼쪽 사이드 파일 따로 빼기
Chasyuss Sep 5, 2025
b5e3a4a
fix: [#303] 스크랩 / 나의 할일 sidebar 제거하고 헤더로 변경
Chasyuss Sep 5, 2025
abf1952
feat: [#303] 할일 추가/취소 시 toast 메시지 나타나기
Chasyuss Sep 5, 2025
e14c43a
feat: [#303] 커뮤니티페이지 무한스크롤 구현
Chasyuss Sep 5, 2025
6e1325c
fix: [#303] 커뮤니티 parameter 수정
Chasyuss Sep 5, 2025
108ed15
Merge pull request #328 from TEAM-DoDream/feature/community-303
Chasyuss Sep 5, 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
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
. "$(dirname "$0")/_/husky.sh"

echo "✅ Lint-staged 시작..."
yarn lint-staged
npx lint-staged
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"prepare": "husky"
},
"lint-staged": {
"ignore": ["src/legacy-components/**"],
"src/**/*.{js,jsx,ts,tsx}": [
"eslint --fix --cache",
"prettier --write"
Expand Down
8 changes: 8 additions & 0 deletions src/amplitude.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
amplitude: any;
}
}

export {};
7 changes: 7 additions & 0 deletions src/assets/icons/AddIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 22 additions & 1 deletion src/common/CheckList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { useDeleteTodoMutation } from '@hook/todo/useDeleteTodoMutation';
import { ReactTagManager } from 'react-gtm-ts';
import { useAddTodoMutation } from '@hook/todo/useAddTodoMutation.ts';
import Plus from '@assets/icons/plus.svg?react';
import AddIcon from '@assets/icons/AddIcon.svg?react';
import BookMarkIcon from '@assets/icons/bookmark.svg?react';
import { useUpdateTodoMutation } from '@hook/todo/useUpdateTodoMutation.ts';

type ChecklistItem = string | { id?: number; text: string };
Expand Down Expand Up @@ -72,6 +74,17 @@ const CheckList = ({
if (!trimmedText) return;

if (isAddingNew) {
// Amplitude 이벤트 - 할일 추가 시도 (inpage)
if (window.amplitude) {
window.amplitude.track('todo_create', {
source_method: 'inpage',
source_page: window.location.pathname,
todo_length: trimmedText.length,
timestamp: new Date().toISOString(),
});
console.log('Amplitude event sent: todo_create_attempt (inpage)');
}

mutate(
{ todoTitle: trimmedText },
{
Expand Down Expand Up @@ -222,7 +235,7 @@ const CheckList = ({

<div className="ml-auto flex min-w-fit items-center gap-[5px]">
{isMyToPage && (
<div className="flex flex-row gap-[5px] opacity-0 transition-opacity duration-200 group-hover:opacity-100">
<div className="flex flex-row gap-[5px]">
{isEditing ? (
<>
<button
Expand Down Expand Up @@ -250,6 +263,14 @@ const CheckList = ({
</>
) : (
<>
<div className="mr-3 flex items-center gap-1 text-gray-500">
<AddIcon className="h-[18px] w-[18px]" />
<span className="text-sm font-B03-SB">3</span>
</div>
<div className="mr-3 flex items-center gap-1 text-gray-500">
<BookMarkIcon className="h-[18px] w-[18px]" />
<span className="text-sm font-B03-SB">999</span>
</div>
<button
onClick={() => handleEdit(idx)}
className="flex items-center gap-[6px] rounded-[10px] bg-gray-100 px-3 py-2 text-gray-500 font-B03-SB"
Expand Down
2 changes: 1 addition & 1 deletion src/common/Divider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ interface DividerProps {
}

const Divider = ({ className }: DividerProps) => {
return <div className={`h-px w-full bg-gray-300 ${className ?? ''}`} />;
return <div className={`h-px w-full ${className ?? ''}`} />;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

기본 배경색 제거로 인한 UI 회귀 가능성 점검

이제 className을 넘기지 않으면 선이 보이지 않을 수 있습니다. 사용처 중 className 미지정 사례를 확인해 주세요.

원한다면 기본 색을 얇게 유지하고, 명시 시 오버라이드하는 방안도 있습니다.

-return <div className={`h-px w-full ${className ?? ''}`} />;
+return <div className={`h-px w-full ${className ?? 'bg-gray-200'}`} />;

🏁 Script executed:

#!/bin/bash
# Divider 사용처에서 className 누락 여부를 확인
rg -nP --type=ts --type=tsx -C2 '<Divider\b(?![^>]*className=)' || true

Length of output: 100


rg 명령이 tsx 파일 타입을 인식하지 못해 검색이 실패한 것으로 보입니다. glob 패턴을 사용해 .ts/.tsx 파일을 대상으로 다시 검색합니다.


🏁 Script executed:

#!/bin/bash
rg -nP '<Divider\b(?![^>]*className=)' -n -C2 -g '*.ts' -g '*.tsx' || true

Length of output: 1549


Divider 컴포넌트 기본 색상 설정 필수

  • className 미지정 시 <Divider />가 투명 선으로 렌더링되어 보이지 않습니다. 다음 5곳에서 확인 필요:
    • src/pages/login/LoginPage.tsx:48
    • src/pages/otherTodo/components/ProfileCard.tsx:63
    • src/pages/home/components/DreamerCard.tsx:73
    • src/common/Footer.tsx:14
    • src/common/Footer.tsx:23
  • className이 없을 때 기본 색상을 적용하거나, 각 사용처에 명시적으로 클래스 지정
-return <div className={`h-px w-full ${className ?? ''}`} />;
+return <div className={`h-px w-full ${className ?? 'bg-gray-200'}`} />;
📝 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
return <div className={`h-px w-full ${className ?? ''}`} />;
export const Divider: React.FC<DividerProps> = ({ className }) => {
return <div className={`h-px w-full ${className ?? 'bg-gray-200'}`} />;
};
🤖 Prompt for AI Agents
In src/common/Divider.tsx around line 6, the Divider renders transparent when no
className is passed; make it apply a sensible default color class (e.g.
bg-gray-200 or bg-neutral-200) when className is undefined while preserving any
provided classes. Change the component to use the provided className if present,
otherwise fall back to the default color class; then check the five usage sites
listed in the comment (src/pages/login/LoginPage.tsx:48,
src/pages/otherTodo/components/ProfileCard.tsx:63,
src/pages/home/components/DreamerCard.tsx:73, src/common/Footer.tsx:14 and :23)
and remove redundant default classes or leave explicit overrides as needed.

};

export default Divider;
16 changes: 15 additions & 1 deletion src/common/FloatingButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,25 @@ const FloatingButton = () => {
}, 2500);
};

const handleFloatingButtonClick = () => {
// Amplitude 이벤트 - 플로팅 버튼 클릭
if (typeof window !== 'undefined' && 'amplitude' in window && window.amplitude) {
(window as any).amplitude.track('todo_create', {
source_method: 'floating',
source_page: window.location.pathname,
timestamp: new Date().toISOString(),
});
console.log('Amplitude event sent: todo_create_start (floating)');
}

setIsModal(true);
};
Comment on lines +21 to +33
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

버튼 내부 중첩 onClick으로 닫기 불가 + ESLint(any) 발생

텍스트 div(onClick)와 버튼(onClick)이 중첩되어 클릭 시 항상 최종적으로 열림(setIsModal(true))으로 귀결됩니다. 또한 (window as any)로 인해 ESLint 에러가 납니다. 열기 시에만 이벤트 트래킹하고, 토글 로직으로 단순화하며 any 캐스트를 제거하세요.

-  const handleFloatingButtonClick = () => {
-    // Amplitude 이벤트 - 플로팅 버튼 클릭
-    if (typeof window !== 'undefined' && 'amplitude' in window && window.amplitude) {
-      (window as any).amplitude.track('todo_create', {
-        source_method: 'floating',
-        source_page: window.location.pathname,
-        timestamp: new Date().toISOString(),
-      });
-      console.log('Amplitude event sent: todo_create_start (floating)');
-    }
-    
-    setIsModal(true);
-  };
+  const handleFloatingButtonClick = () => {
+    const opening = !isModal;
+    if (opening && typeof window !== 'undefined' && window.amplitude) {
+      window.amplitude.track('todo_create', {
+        source_method: 'floating',
+        source_page: window.location.pathname,
+        timestamp: new Date().toISOString(),
+      });
+      console.log('Amplitude event sent: todo_create (floating)');
+    }
+    setIsModal((prev) => !prev);
+  };
@@
-      <button
+      <button
+        type="button"
         className="fixed bottom-[50px] right-[80px] flex h-[102px] items-center gap-[10px] rounded-[28px] bg-purple-500 px-[30px] shadow-shadow4 transition-colors hover:bg-purple-600"
         onClick={handleFloatingButtonClick}
       >
-        <div
-          className="cursor-pointer text-white font-T02-B"
-          onClick={() => setIsModal(false)}
-        >
+        <div className="cursor-pointer text-white font-T02-B">
           {isModal ? '할일 추가 닫기' : '바로 할일 추가'}
         </div>

Also applies to: 37-37, 41-46

🧰 Tools
🪛 ESLint

[error] 24-24: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
In src/common/FloatingButton.tsx around lines 21-33 (also apply same changes at
37 and 41-46): the current nested onClick handlers cause clicks to always open
the modal and the code uses (window as any) which triggers ESLint any errors;
change the click handling to a single toggle using setIsModal(prev => { const
next = !prev; if (next) { const amp = (window as Window & { amplitude?: {
track?: (event: string, props?: Record<string, any>) => void } }).amplitude;
amp?.track('todo_create', { source_method: 'floating', source_page:
window.location.pathname, timestamp: new Date().toISOString() });
console.log('Amplitude event sent: todo_create_start (floating)'); } return
next; }); remove the nested onClick on the inner div (or stopPropagation) so a
single handler controls open/close, and replace any casts with a typed Window
assertion and optional chaining for amplitude.


return (
<div>
<button
className="fixed bottom-[50px] right-[80px] flex h-[102px] items-center gap-[10px] rounded-[28px] bg-purple-500 px-[30px] shadow-shadow4 transition-colors hover:bg-purple-600"
onClick={() => setIsModal(true)}
onClick={handleFloatingButtonClick}
>
<div
className="cursor-pointer text-white font-T02-B"
Expand Down
9 changes: 7 additions & 2 deletions src/common/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ const NavItems = [
label: '직업 정보',
path: '/jobfound',
match: (pathname: string) =>
pathname.startsWith('/jobfound') && pathname !== '/jobsearch',
pathname.startsWith('/jobfound') ||
(pathname.startsWith('/jobinfo') && pathname !== '/jobsearch'),
},
//배움터 찾기
{ label: '학원 정보', path: '/learning' },
Expand All @@ -28,11 +29,15 @@ const NavItems = [
pathname.startsWith('/others') || pathname.startsWith('/community'),
},

{
label: '스크랩',
path: '/mytodo/scrap',
},

//마이드림
{
label: '나의 할일',
path: '/mytodo/list',
match: (pathname: string) => pathname.startsWith('/mytodo'),
},
];

Expand Down
23 changes: 23 additions & 0 deletions src/common/Tag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
interface TagProps {
context: string;
fontClass?: string;
width?: string;
height?: string;
}

const Tag = ({
context,
fontClass = 'font-T05-SB',
width = 'w-auto',
height = 'h-auto',
}: TagProps) => {
return (
<div
className={`flex items-center justify-center rounded-[10px] bg-purple-100 p-2 text-purple-500 ${fontClass} ${width} ${height}`}
>
{context}
</div>
);
};

export default Tag;
67 changes: 56 additions & 11 deletions src/common/modal/FloatingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { ReactTagManager } from 'react-gtm-ts';
import { useAddTodoMutation } from '@hook/todo/useAddTodoMutation';
import Fire from '@assets/icons/fire.svg?react';
import Reset from '@assets/icons/reset.svg?react';
import {
useFloatingPopular,
useNoLoginFloatingPopular,
} from '@hook/floating/useFloatingPopular';

interface FloatingModalProps {
onClose: () => void;
Expand All @@ -22,6 +26,27 @@ const FloatingModal = ({ onClose, onAddTask }: FloatingModalProps) => {

const { mutate } = useAddTodoMutation();
const { data: hasJob, isLoading } = useMdTodoQuery();
const {
data: hot,
isLoading: loadingLogin,
refetch: refetchLogin,
} = useFloatingPopular({
enabled: isLoggedIn,
queryKey: [],
});
Comment on lines +33 to +36
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

중요: 옵션으로 queryKey를 덮어써 캐시 키가 비어 충돌합니다

useFloatingPopular/useNoLoginFloatingPopular 내부에서 queryKey를 지정한 뒤 ...options로 덮어쓰고 있어, 여기서 빈 배열을 전달하면 캐시 키가 []로 고정됩니다. 이는 서로 다른 쿼리가 같은 캐시를 공유하거나 경고를 유발할 수 있습니다. 옵션에서 queryKey를 제거하세요.

   } = useFloatingPopular({
     enabled: isLoggedIn,
-    queryKey: [],
   });
 ...
   } = useNoLoginFloatingPopular({
     enabled: !isLoggedIn,
-    queryKey: [],
   });

Also applies to: 42-45

🤖 Prompt for AI Agents
In src/common/modal/FloatingModal.tsx around lines 33-36 (and similarly lines
42-45), the call to useFloatingPopular/useNoLoginFloatingPopular is passing
queryKey: [] which overwrites the hook's internal cache key and fixes it to an
empty array; remove the queryKey property from the options object (i.e., stop
supplying queryKey: [] or undefined) so the hook's internally defined queryKey
is used, leaving the rest of the options/spread intact.


const {
data: hotGuest,
isLoading: loadingGuest,
refetch: refetchGuest,
} = useNoLoginFloatingPopular({
enabled: !isLoggedIn,
queryKey: [],
});

const hotList = isLoggedIn ? hot : hotGuest;
const isHotLoading = isLoggedIn ? loadingLogin : loadingGuest;
const refreshHot = isLoggedIn ? refetchLogin : refetchGuest;

const hasNoJob =
isLoggedIn &&
Expand All @@ -34,6 +59,17 @@ const FloatingModal = ({ onClose, onAddTask }: FloatingModalProps) => {
const handleSubmit = () => {
if (!isLoggedIn || !taskText.trim() || hasNoJob) return;

// Amplitude 이벤트 - 할일 추가 시도
if (window.amplitude) {
window.amplitude.track('todo_create', {
source_method: 'floating',
source_page: location.pathname,
todo_length: taskText.trim().length,
timestamp: new Date().toISOString(),
});
console.log('Amplitude event sent: todo_create_attempt');
}

mutate(
{
todoTitle: taskText,
Expand Down Expand Up @@ -61,20 +97,29 @@ const FloatingModal = ({ onClose, onAddTask }: FloatingModalProps) => {
<div className="text-black font-T03-B">바로 할일 추가</div>
</div>

<div className="mt-[22px] flex w-full flex-row items-center rounded-[8px] bg-gray-100 p-3">
<div className="flex flex-row items-center gap-[6px]">
<Fire />
<div className="text-gray-900 font-B03-SB"> 인기</div>
</div>
<div className="mt-[22px] w-full rounded-[8px] bg-gray-100 p-3">
<div className="flex items-center">
<div className="flex shrink-0 items-center gap-[6px]">
<Fire />
<span className="text-gray-900 font-B03-SB">인기</span>
</div>

<div className="mx-[10px] h-4 w-[1px] bg-gray-400" />
<div className="mx-[10px] h-4 w-[1px] shrink-0 bg-gray-400" />

<div className="text-gray-600 font-B03-M">
요양보호사 카페 주소 검색하고 가입하기
</div>
<div className="min-w-0 flex-1">
<span className="block truncate text-gray-600 font-B03-M">
{isHotLoading
? '인기 목록을 불러오는 중입니다...'
: (hotList?.title ?? '인기데이터가 없습니다')}
</span>
</div>

<div className="ml-[83px] cursor-pointer">
<Reset />
<div
className="shrink-0 cursor-pointer"
onClick={() => refreshHot()}
>
<Reset />
</div>
</div>
</div>

Expand Down
79 changes: 79 additions & 0 deletions src/hook/community/query/useCommunityGetTodo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import api from '@hook/api';
import { useInfiniteQuery } from '@tanstack/react-query';

export interface CommunityItem {
id: number;
name: string;
level: string;
imageUrl?: string;
dDay?: string;
description: string;
saveCount: number;
isSaved: boolean;
}

export interface CommunityGetTodoResponse {
content: CommunityItem[];
number: number;
size: number;
first: boolean;
last: boolean;
empty: boolean;
numberOfElements: number;
pageable: {
pageNumber: number;
};
sort: { empty: boolean; sorted: boolean; unsorted: boolean };
}

type BaseParams = {
jobName: string;
level: string;
sort: string;
size: number;
};

const fetchCommunityPage = async ({
jobName,
level,
sort,
page,
size,
}: BaseParams & { page: number }): Promise<CommunityGetTodoResponse> => {
const token =
typeof window !== 'undefined' ? localStorage.getItem('accessToken') : null;

const res = await api.get('/v1/community/todos', {
params: { jobName, level, sort, page, size },
...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
});
const result = res.data?.data;

return {
content: result.content,
number: result.number,
size: result.size,
first: result.first,
last: result.last,
empty: result.empty,
numberOfElements: result.numberOfElements,
pageable: { pageNumber: result.pageable?.pageNumber ?? page },
sort: result.sort,
};
};
Comment on lines +46 to +63
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

런타임 형태 검증과 방어적 체크 부재 — 응답 파싱 추가 제안

result가 null/shape 변경 시 NPE 위험이 있습니다. Zod 파싱과 명시적 에러를 추가하세요.

원하시면 해당 스키마를 별도 파일로 분리해 드리겠습니다.

 import api from '@hook/api';
+import { z } from 'zod';
@@
-  const res = await api.get('/v1/community/todos', {
+  const res = await api.get('/v1/community/todos', {
     params: { jobName, level, sort, page, size },
     ...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
   });
-  const result = res.data?.data;
-
-  return {
-    content: result.content,
-    number: result.number,
-    size: result.size,
-    first: result.first,
-    last: result.last,
-    empty: result.empty,
-    numberOfElements: result.numberOfElements,
-    pageable: { pageNumber: result.pageable?.pageNumber ?? page },
-    sort: result.sort,
-  };
+  const result = res.data?.data;
+  if (!result) {
+    throw new Error('Invalid response: data is missing');
+  }
+
+  const CommunityItemSchema = z
+    .object({
+      id: z.number().int().positive(),
+      name: z.string(),
+      level: z.string(),
+      imageUrl: z.string().url().or(z.literal('')).optional(),
+      dDay: z.string().optional(),
+      description: z.string(),
+      saveCount: z.number().int().nonnegative(),
+      isSaved: z.boolean(),
+    })
+    .passthrough();
+
+  const CommunityGetTodoResponseSchema = z
+    .object({
+      content: z.array(CommunityItemSchema),
+      number: z.number().int().nonnegative(),
+      size: z.number().int().positive(),
+      first: z.boolean(),
+      last: z.boolean(),
+      empty: z.boolean(),
+      numberOfElements: z.number().int().nonnegative(),
+      pageable: z.object({ pageNumber: z.number().int().nonnegative() }).partial(),
+      sort: z.object({ empty: z.boolean(), sorted: z.boolean(), unsorted: z.boolean() }),
+    })
+    .passthrough();
+
+  const parsed = CommunityGetTodoResponseSchema.parse(result);
+  return {
+    ...parsed,
+    pageable: { pageNumber: parsed.pageable?.pageNumber ?? page },
+  };
📝 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 res = await api.get('/v1/community/todos', {
params: { jobName, level, sort, page, size },
...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
});
const result = res.data?.data;
return {
content: result.content,
number: result.number,
size: result.size,
first: result.first,
last: result.last,
empty: result.empty,
numberOfElements: result.numberOfElements,
pageable: { pageNumber: result.pageable?.pageNumber ?? page },
sort: result.sort,
};
};
// src/hook/community/query/useCommunityGetTodo.ts
import api from '@hook/api';
import { z } from 'zod';
export const useCommunityGetTodo = async ({
jobName,
level,
sort,
page,
size,
token,
}: {
jobName: string;
level: string;
sort: string;
page: number;
size: number;
token?: string;
}) => {
const res = await api.get('/v1/community/todos', {
params: { jobName, level, sort, page, size },
...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
});
const result = res.data?.data;
if (!result) {
throw new Error('Invalid response: data is missing');
}
const CommunityItemSchema = z
.object({
id: z.number().int().positive(),
name: z.string(),
level: z.string(),
imageUrl: z.string().url().or(z.literal('')).optional(),
dDay: z.string().optional(),
description: z.string(),
saveCount: z.number().int().nonnegative(),
isSaved: z.boolean(),
})
.passthrough();
const CommunityGetTodoResponseSchema = z
.object({
content: z.array(CommunityItemSchema),
number: z.number().int().nonnegative(),
size: z.number().int().positive(),
first: z.boolean(),
last: z.boolean(),
empty: z.boolean(),
numberOfElements: z.number().int().nonnegative(),
pageable: z
.object({ pageNumber: z.number().int().nonnegative() })
.partial(),
sort: z
.object({
empty: z.boolean(),
sorted: z.boolean(),
unsorted: z.boolean(),
}),
})
.passthrough();
const parsed = CommunityGetTodoResponseSchema.parse(result);
return {
...parsed,
pageable: { pageNumber: parsed.pageable?.pageNumber ?? page },
};
};


export const useCommunityGetTodo = (params: BaseParams) => {
return useInfiniteQuery({
queryKey: ['CommunityGetTodo', params],
initialPageParam: 0,
queryFn: ({ pageParam }) =>
fetchCommunityPage({ ...params, page: pageParam as number }),

getNextPageParam: (lastPage) => {
if (lastPage.last) return undefined;
return lastPage.number + 1;
},

retry: 0,
});
};
26 changes: 26 additions & 0 deletions src/hook/community/query/useGetCommunityQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useQuery } from '@tanstack/react-query';
import api from '@hook/api';

export const useGetCommunityQuery = () => {
const isLoggedIn =
typeof window !== 'undefined' && !!localStorage.getItem('accessToken');

return useQuery({
queryKey: ['community', isLoggedIn],
queryFn: async () => {
Comment on lines +8 to +10
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

queryKey에 로그인 여부만 포함 → 계정 전환 캐시 오염 가능성

동일한 “로그인 상태(true)”에서 계정을 바꾸면 key가 동일해 캐시 재사용됩니다. 사용자 식별자(예: userId) 또는 토큰 버전 키를 포함해 주세요. 토큰 원문 자체는 포함하지 마세요.

예) queryKey: ['community', { authed: !!token, userId }]


queryKey에 userId 포함 필수
현재 queryKey: ['community', isLoggedIn]만 사용 → 로그인 유지 상태에서 계정 전환 시 캐시가 재사용되어 잘못된 데이터 노출 위험.
queryKeyuserId(또는 토큰 버전 등 식별자) 추가하세요. 토큰 원문은 제외합니다.
예)

queryKey: ['community', { authed: !!token, userId }]
🤖 Prompt for AI Agents
In src/hook/community/query/useGetCommunityQuery.ts around lines 8-10, the
queryKey currently omits a user identifier which can cause cache reuse across
account switches; update the queryKey to include a non-sensitive user identifier
(e.g. userId or token version) rather than the raw token. Retrieve userId from
the auth context or a safe selector, and set queryKey like ['community', {
authed: !!token, userId }] (or similar) so cache is partitioned per-user; do not
store or log the raw token.

const token =
typeof window !== 'undefined'
? localStorage.getItem('accessToken')
: null;

const res = await api.get(
'/v1/community/job',
token ? { headers: { Authorization: `Bearer ${token}` } } : undefined
);

return res.data;
},
staleTime: 1000 * 60 * 5,
refetchOnWindowFocus: false,
});
};
Loading