-
Notifications
You must be signed in to change notification settings - Fork 1
[✨FEATURE] 커뮤니티 api 왼쪽 #327
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
Conversation
|
Caution Review failedThe pull request is closed. Walkthrough커뮤니티 페이지를 데이터 기반으로 전환했다. 직무 선택 드롭다운과 Zustand 전역 상태를 도입했고, 두 가지 React Query 훅을 추가해 직무 목록(/v1/community/job)과 선택 직무의 HOT 할 일(/v1/community/todos/popular)을 조회한다. Community 페이지는 해당 데이터로 UI를 동적으로 렌더링한다. 전역 스토어는 Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as 사용자
participant Page as Community.tsx
participant Dropdown as CommunityDropdown
participant Store as useCommunityStore
participant HookJobs as useGetCommunityQuery
participant HookHot as useGetHotPopularQuery
participant API as Backend API
User->>Page: 페이지 진입
Page->>Dropdown: 드롭다운 렌더
activate Dropdown
Dropdown->>HookJobs: GET /v1/community/job 요청
HookJobs->>API: GET /v1/community/job (Bearer 토큰 포함 가능)
API-->>HookJobs: 직무 목록 응답
HookJobs-->>Dropdown: 직무 데이터 반환
Dropdown->>Store: setSelectedJob({ name, id })로 초기 선택 설정
deactivate Dropdown
Note over Page,HookHot: 선택 직무명 변경 시
Page->>Store: selectedJobName 구독
Store-->>Page: 선택된 직무명
Page->>HookHot: GET /v1/community/todos/popular?jobName=...
HookHot->>API: 인기 할 일 요청
API-->>HookHot: 인기 할 일 배열 응답
HookHot-->>Page: 인기 항목 전달
Page-->>User: HOT 리스트 렌더링
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (4)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
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.
Actionable comments posted: 1
🧹 Nitpick comments (17)
src/utils/data/community/jobs.ts (2)
1-24: 정적 옵션 vs API 데이터 드리프트 방지드롭다운 옵션이 정적 배열이고 기본값만 API에서 가져오면, API가 제공하는 직무와 목록이 어긋날 수 있습니다. 가능하면 옵션 자체도 API 응답을 우선 사용하고, 이 파일은 오프라인/폴백 용도로만 쓰는 구조를 권장합니다. CommunityDropdown으로 API 리스트를 전달하는 경로를 고려해 주세요.
1-24: 리터럴 타입 고정(as const) 및 타입 내보내기런타임/타입 안정성을 위해 읽기 전용 배열과 유니온 타입을 노출해두면 이후 스토어/훅 간 타입 일관성이 좋아집니다.
-const jobs: string[] = [ +export const jobs = [ '요양보호사', '간호조무사', '보육교사', '사회복지사', '직업상담사', '심리상담사', '급식 도우미', '사무보조원', '회계사무원', '수의테크니션', '웨딩 헬퍼', '미용사 (일반)', '미용사 (피부)', '미용사 (네일)', '미용사 (메이크업)', '반려동물미용사', '레크리에이션 지도사', '바리스타', '공인중개사', '산후조리사', -]; - -export default jobs; + ] as const; + +export type JobName = typeof jobs[number]; +export default jobs;src/hook/community/query/useGetCommunityQuery.ts (3)
5-10: queryKey에 불린만 사용하면 토큰 교체 시 재요청이 막힐 수 있음토큰 갱신(진리값은 그대로 true) 시 캐시 키가 동일해 재조회가 발생하지 않습니다. 토큰(또는 tokenVersion/userId 등 비민감 식별자)을 키에 포함하거나, 로그인/갱신 시점에 invalidate를 트리거하세요.
-export const useGetCommunityQuery = () => { - const isLoggedIn = - typeof window !== 'undefined' && !!localStorage.getItem('accessToken'); +export const useGetCommunityQuery = () => { + const token = + typeof window !== 'undefined' ? localStorage.getItem('accessToken') : null; + const isLoggedIn = !!token; return useQuery({ - queryKey: ['community', isLoggedIn], + // 민감정보 노출이 우려되면 token 대신 tokenVersion/userId를 사용하세요. + queryKey: ['community', isLoggedIn, token ?? null],
11-19: 토큰 재조회 중복 + 헤더 부착 공통화토큰을 위에서 구했으니 중복 조회를 제거하세요. 가능하면 Axios 인터셉터에서 Authorization을 공통으로 붙여 개별 훅에서 분기하지 않는 것을 추천합니다.
- queryFn: async () => { - const token = - typeof window !== 'undefined' - ? localStorage.getItem('accessToken') - : null; - - const res = await api.get( - '/v1/community/job', - token ? { headers: { Authorization: `Bearer ${token}` } } : undefined - ); + queryFn: async () => { + const res = await api.get( + '/v1/community/job', + token ? { headers: { Authorization: `Bearer ${token}` } } : undefined + );
21-25: 반환 타입 명시로 사용처 혼동 최소화
res.data내부에 다시data가 있는 형태라면 제네릭/타입을 선언해return을 일관화하세요(예:select로 최종 형태까지 매핑). 현재 CommunityDropdown에서communityData?.data접근을 가정하고 있어 계약 고정이 중요합니다.src/store/useCommunityStore.ts (2)
3-11: JobName 유니온 타입으로 상태를 좁혀 타입 안정성 강화정적 목록/API 계약이 정해져 있다면
JobName유니온으로 제한하세요. 초기값은 ''로 두되 setter는JobName만 받도록 하면 오탈자/미지원 값 유입을 차단할 수 있습니다.-import { create } from 'zustand'; +import { create } from 'zustand'; +import type { JobName } from '@utils/data/community/jobs'; interface CommunityStore { - selectedJobName: string; - setSelectedJobName: (jobName: string) => void; + selectedJobName: '' | JobName; + setSelectedJobName: (jobName: JobName) => void; + resetSelectedJob: () => void; } export const useCommunityStore = create<CommunityStore>((set) => ({ selectedJobName: '', - setSelectedJobName: (jobName: string) => set({ selectedJobName: jobName }), + setSelectedJobName: (jobName: JobName) => set({ selectedJobName: jobName }), + resetSelectedJob: () => set({ selectedJobName: '' }), }));
8-11: 선택 유지가 필요하면 persist 미들웨어 고려새로고침/탭 재방문 시 선택값을 유지하려면
persist를 도입하세요(스토리지 키/버전만 신경 쓰면 됩니다).src/pages/community/Community.tsx (5)
1-1: 주석 처리된 미사용 import 정리dead code는 제거하세요.
-// import DropDownIcon from '@assets/icons/drop_down.svg?react';
17-23: 초기 value/콘솔 로그 제거로 단순화CommunityDropdown이 스토어를 직접 갱신하므로 부모
value=jobs[0]는 불필요하며console.log는 제거하세요.- <CommunityDropdown - options={jobs} - value={jobs[0]} - onSelect={(value) => { - console.log(value); - }} - /> + <CommunityDropdown options={jobs} onSelect={() => {}} />
34-37: 빈 선택 시 헤더 가독성 개선선택 전엔 플레이스홀더를 표시하세요.
- <div className="text-black font-T04-B"> - {' '} - {selectedJobName} HOT 할 일 - </div> + <div className="text-black font-T04-B"> + {(selectedJobName || '직무 선택')} HOT 할 일 + </div>
39-75: 빈 목록/로딩 상태 처리데이터 없을 때의 UX를 보완하세요(간단 안내문 또는 스켈레톤).
- {popularTodos.map((item, idx) => ( + {popularTodos.length === 0 ? ( + <div className="w-full py-6 text-center text-gray-400 font-C01-R"> + 표시할 항목이 없습니다. + </div> + ) : ( + popularTodos.map((item, idx) => ( <div key={item.id} className="flex w/full flex-row items-start justify-between py-4" > <div className="flex flex-row items-center gap-[15px]"> <div className="text-purple-500 font-T05-SB">{idx + 1}</div> <div className="flex flex-row items-center gap-[15px]"> <img - src={item.imageUrl} + src={item.imageUrl} + loading="lazy" alt="프로필이미지" className="h-[30px] w-[30px] rounded-full bg-gray-50" /> <div className="flex flex-col"> <div className="flex w-full flex-row items-start justify-between gap-[10px]"> <div className="flex-1 break-words text-black font-B01-SB"> {item.description} </div> <div className="mr-2 shrink-0 whitespace-nowrap text-gray-500 font-C01-R"> {item.dDay} </div> </div> <div className="mt-1 text-gray-500 font-C01-R"> {item.name} </div> </div> </div> </div> <div className="flex flex-row items-center gap-[6px] text-purple-500"> <Bookmark /> <span className="font-B03-SB">{item.saveCount}</span> </div> </div> - ))} + ))} + )}
7-12: 정적 옵션과 API 기본값 불일치 가능성 점검API가 반환한 기본 직무가
jobs배열에 없으면 선택 라벨과 옵션 목록이 어긋납니다. 옵션도 API 리스트를 전달하는 구조로 이관하는 것을 권장합니다. 또한useGetHotPopularQuery내부console.log도 제거해 주세요.src/hook/community/query/useGetHotPopularQuery.ts (2)
29-35: 불필요한 콘솔 출력 제거 및 응답 타입 안전화(필요 시 토큰 헤더 추가)운영에서
console.log는 지양하고, Axios 제네릭으로 응답을 안전하게 타이핑하세요. 엔드포인트가 인증 필요할 수 있으니 토큰이 있으면 헤더에 동봉하도록 제안합니다.다음처럼 정리하면 됩니다:
queryFn: async () => { - const res = await api.get('/v1/community/todos/popular', { - params: { jobName: selectedJobName }, - }); - console.log(res.data); - const body = res.data as HotPopularApiResponse; - return Array.isArray(body?.data) ? body.data : []; + const token = + typeof window !== 'undefined' + ? localStorage.getItem('accessToken') + : null; + + const res = await api.get<HotPopularApiResponse>( + '/v1/community/todos/popular', + { + params: { jobName: selectedJobName }, + ...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}), + } + ); + if (process.env.NODE_ENV === 'development') { + // eslint-disable-next-line no-console + console.debug('[useGetHotPopularQuery]', res.data); + } + return Array.isArray(res.data?.data) ? res.data.data : []; },
36-38: 초기/플레이스홀더 데이터 지정으로 널 가드 단순화소비 측에서
data ?? []패턴을 줄이려면placeholderData로 빈 배열을 넣어 두세요.staleTime: 1000 * 60 * 5, refetchOnWindowFocus: false, + placeholderData: [] as HotPopularItem[],src/pages/community/components/CommunityDropdown.tsx (3)
24-24: 초기 렌더에서 플레이스홀더를 보이게 하려면 기본값을 ''로 시작현재는 API 응답 전에도
options[0]이 라벨로 노출됩니다. UX 관점에서 ‘선택’ 플레이스홀더를 유지하려면 기본값을''로 두는 편이 자연스럽습니다.- const [selected, setSelected] = useState<T | ''>(value || (options[0] ?? '')); + const [selected, setSelected] = useState<T | ''>(value || '');
67-75: 접근성: 토글 영역에 버튼 롤/키보드 핸들링 추가키보드 접근성을 위해 role/tabIndex/Enter·Space 처리 추가를 권장합니다.
- <div className="flex flex-row items-center gap-[18px]" onClick={toggle}> + <div + className="flex flex-row items-center gap-[18px]" + role="button" + tabIndex={0} + onClick={toggle} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') toggle(); + }} + >
43-51: API 응답 가공 로직의 타입 안정성 보완
unknown기반 캐스팅 대신 지역 타입을 정의해 좁히면 안전합니다. 예:type CommunityData = { data?: string | { jobName?: string }[] }.- const raw = (communityData as { data?: unknown } | undefined)?.data; + type CommunityData = { data?: string | { jobName?: string }[] }; + const raw = (communityData as CommunityData | undefined)?.data;
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (6)
src/hook/community/query/useGetCommunityQuery.ts(1 hunks)src/hook/community/query/useGetHotPopularQuery.ts(1 hunks)src/pages/community/Community.tsx(2 hunks)src/pages/community/components/CommunityDropdown.tsx(1 hunks)src/store/useCommunityStore.ts(1 hunks)src/utils/data/community/jobs.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/hook/community/query/useGetHotPopularQuery.ts (1)
src/store/useCommunityStore.ts (1)
useCommunityStore(8-11)
src/pages/community/components/CommunityDropdown.tsx (3)
src/hook/useDropdown.ts (1)
useDropdown(3-33)src/hook/community/query/useGetCommunityQuery.ts (1)
useGetCommunityQuery(4-26)src/store/useCommunityStore.ts (1)
useCommunityStore(8-11)
src/pages/community/Community.tsx (3)
src/store/useCommunityStore.ts (1)
useCommunityStore(8-11)src/hook/community/query/useGetHotPopularQuery.ts (1)
useGetHotPopularQuery(22-39)src/pages/community/components/CommunityDropdown.tsx (1)
CommunityDropdown(16-106)
🔇 Additional comments (2)
src/hook/community/query/useGetHotPopularQuery.ts (1)
25-28: queryKey/enable 조건 구성 적절직무 선택값에 종속된 queryKey와 enabled 가드가 정확합니다. 불필요한 호출을 잘 막습니다.
src/pages/community/components/CommunityDropdown.tsx (1)
30-41: 부모가 첫 번째 옵션값을 명시적으로 지정하는 경우 무시될 수 있음
value && value !== (options[0] as T)조건 때문에, 부모가 런타임에 의도적으로options[0]을 설정해도 반영되지 않을 수 있습니다. “초기 마운트 한정 무시”가 목적이라면 마운트 여부 플래그로 1회만 스킵하도록 조정하는지 확인 부탁드립니다.원하시면
useRef로 초깃값만 스킵하는 패턴으로 리팩터 제안드릴게요.
| useEffect(() => { | ||
| if (!initializedFromApi) return; // API 초기 반영 전에는 옵션/외부값으로 덮어쓰지 않음 | ||
| // 부모가 초기 표시용으로 전달한 첫 옵션 값은 무시하고, 실제 변경만 반영 | ||
| if (value && value !== (options[0] as T)) { | ||
| setSelected(value); | ||
| return; | ||
| } | ||
| if (!userSelected) { | ||
| setSelected((prev) => prev || (options[0] ?? '')); | ||
| } | ||
| }, [value, options, userSelected, initializedFromApi]); | ||
|
|
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.
전역 스토어 동기화 누락: 외부 value 변화 시 selectedJobName이 갱신되지 않음
value가 부모에서 변경될 때 로컬 selected만 바뀌고 setSelectedJobName이 호출되지 않아, 전역 상태를 사용하는 다른 훅(useGetHotPopularQuery)과 UI가 불일치할 수 있습니다.
간단히 selected 변경을 전역 스토어와 항상 동기화하는 이펙트를 추가하는 방식을 권장합니다(중복 호출 안전).
const { setSelectedJobName } = useCommunityStore();
+ // 로컬 선택값이 바뀌면 항상 전역 스토어와 동기화
+ useEffect(() => {
+ if (selected !== '') {
+ setSelectedJobName(String(selected));
+ }
+ }, [selected, setSelectedJobName]);추가로, 위 이펙트를 도입하면 아래 두 곳의 직접 호출은 제거해도 됩니다(중복 디스패치 감소).
- Line 56:
setSelectedJobName(String(apiDefault)); - Line 91:
setSelectedJobName(String(opt));
원하시면 제거용 diff도 드리겠습니다.
📝 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.
| useEffect(() => { | |
| if (!initializedFromApi) return; // API 초기 반영 전에는 옵션/외부값으로 덮어쓰지 않음 | |
| // 부모가 초기 표시용으로 전달한 첫 옵션 값은 무시하고, 실제 변경만 반영 | |
| if (value && value !== (options[0] as T)) { | |
| setSelected(value); | |
| return; | |
| } | |
| if (!userSelected) { | |
| setSelected((prev) => prev || (options[0] ?? '')); | |
| } | |
| }, [value, options, userSelected, initializedFromApi]); | |
| const { setSelectedJobName } = useCommunityStore(); | |
| // 로컬 선택값이 바뀌면 항상 전역 스토어와 동기화 | |
| useEffect(() => { | |
| if (selected !== '') { | |
| setSelectedJobName(String(selected)); | |
| } | |
| }, [selected, setSelectedJobName]); | |
| useEffect(() => { | |
| if (!initializedFromApi) return; // API 초기 반영 전에는 옵션/외부값으로 덮어쓰지 않음 | |
| // 부모가 초기 표시용으로 전달한 첫 옵션 값은 무시하고, 실제 변경만 반영 | |
| if (value && value !== (options[0] as T)) { | |
| setSelected(value); | |
| return; | |
| } | |
| if (!userSelected) { | |
| setSelected((prev) => prev || (options[0] ?? '')); | |
| } | |
| }, [value, options, userSelected, initializedFromApi]); |
🤖 Prompt for AI Agents
In src/pages/community/components/CommunityDropdown.tsx around lines 30 to 41,
the effect updates local selected when props change but never syncs local
selected to the global store, causing selectedJobName to lag; add a new
useEffect that runs when selected changes and always calls
setSelectedJobName(String(selected)) (ensure it is safe for duplicate calls),
and after adding this syncing effect remove the redundant direct dispatches at
lines 56 (`setSelectedJobName(String(apiDefault));`) and 91
(`setSelectedJobName(String(opt));`) to avoid duplicate updates.
Chasyuss
left a comment
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.
확인했어용
🚀 풀 리퀘스트 제안
📋 작업 내용
커뮤니티 왼쪽 페이지 api 연결 작업
📸 스크린샷 (선택 사항)
📄 기타
Summary by CodeRabbit