-
Notifications
You must be signed in to change notification settings - Fork 0
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
[feat] 명함 상세 페이지 구현 #74
Conversation
📝 WalkthroughWalkthrough이번 PR은 package.json 구성 업데이트와 함께 card-detail 관련 UI 컴포넌트, 커스텀 훅, 타입, 설정 파일을 추가 및 수정하였습니다. 신규 의존성 추가, 데이터 구조 단순화, 이미지 소스 변경, 레이아웃 및 스타일링 개선 등이 포함되어 있습니다. 또한 카드 상세 페이지 렌더링, 스크롤 및 탭 상호작용, API 데이터 페칭(현재 모킹) 등의 기능 흐름이 구현되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant U as 사용자
participant P as Page 컴포넌트
participant H as CardDetailHeader
participant Q as useCardDetailQuery
participant API as API (모킹 데이터)
U->>P: 카드 상세 페이지 진입
P->>H: 헤더 컴포넌트 렌더링
H->>Q: 카드 정보 요청 (cardId 전달)
Q->>API: 카드 상세 데이터 요청
API-->>Q: 모킹 데이터 응답
Q-->>H: 데이터 전달
H->>U: 동적 헤더 렌더링 (JOB_CONFIG 적용)
sequenceDiagram
participant U as 사용자
participant T as CardTabs 컴포넌트
participant S as useScrollPosition
participant A as useTabsActive
U->>T: 탭 클릭 또는 스크롤
T->>S: 섹션 보임 상태 관찰 요청
S-->>T: 인터섹션 상태 업데이트
T->>A: 현재 뷰 상태 동기화 요청
A-->>T: 활성 탭 상태 설정
T->>U: 업데이트된 탭 및 콘텐츠 렌더링
Suggested labels
Suggested reviewers
Tip ⚡🧪 Multi-step agentic review comment chat (experimental)
📜 Recent review detailsConfiguration used: .coderabbit.yml ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
🔇 Additional comments (1)
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
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.
Caution
Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
🛑 Comments failed to post (22)
src/features/home/components/BusinessCard/Background/AddCradBackground.tsx (1)
1-3:
⚠️ Potential issue파일 이름에 오타가 있습니다.
현재 파일 이름이
AddCradBackground.tsx
로 되어 있는데, 이는AddCardBackground.tsx
의 오타로 보입니다.파일 이름을
AddCardBackground.tsx
로 수정하는 것이 좋겠습니다.src/features/home/providers/CardSwiperProvider.tsx (1)
1-4: 🛠️ Refactor suggestion
Swiper CSS가 import 되지 않았습니다.
Swiper 컴포넌트를 사용할 때는 필요한 CSS 파일도 함께 import 해야 합니다. 그렇지 않으면 Swiper가 제대로 스타일링되지 않을 수 있습니다.
'use client'; import { PropsWithChildren } from 'react'; import { Swiper } from 'swiper/react'; +import 'swiper/css'; +import 'swiper/css/pagination';📝 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.'use client'; import { PropsWithChildren } from 'react'; import { Swiper } from 'swiper/react'; import 'swiper/css'; import 'swiper/css/pagination';
src/features/home/components/BusinessCard/AddCard.tsx (1)
5-12: 🛠️ Refactor suggestion
컴포넌트에 클릭 핸들러가 없습니다.
AddCard 컴포넌트는 사용자가 클릭하여 새 카드를 추가할 수 있게 하는 목적으로 보이지만, 클릭 이벤트 핸들러가 없습니다. 클릭 시 적절한 동작을 수행하는 핸들러를 추가하고, 접근성을 위해 적절한 속성(role, aria-label 등)을 포함하는 것이 좋습니다.
- export const AddCard = () => { + export const AddCard = ({ onClick }: { onClick?: () => void }) => { return ( <div + onClick={onClick} + role="button" + tabIndex={0} + aria-label="새 명함 추가하기" + onKeyDown={(e) => e.key === 'Enter' && onClick?.()} className="bg-[rgba(255,255,255, 0.2)] relative flex h-[394px] w-[270px] flex-col items-center justify-center gap-2 overflow-hidden rounded-2xl"> <Typography variant="body-1">추가하기</Typography> <CardPlusIcon /> </div> ); };📝 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.export const AddCard = ({ onClick }: { onClick?: () => void }) => { return ( <div onClick={onClick} role="button" tabIndex={0} aria-label="새 명함 추가하기" onKeyDown={(e) => e.key === 'Enter' && onClick?.()} className="bg-[rgba(255,255,255, 0.2)] relative flex h-[394px] w-[270px] flex-col items-center justify-center gap-2 overflow-hidden rounded-2xl"> <Typography variant="body-1">추가하기</Typography> <CardPlusIcon /> </div> ); };
src/app/layout.tsx (2)
14-19: 🛠️ Refactor suggestion
사용자 확대 제한에 대한 접근성 문제
뷰포트 설정에서
userScalable: false
와maximumScale: 1
은 사용자가 페이지를 확대하는 기능을 제한합니다. 이는 시각 장애가 있는 사용자에게 접근성 문제를 일으킬 수 있습니다.접근성 지침(WCAG)에 따르면 사용자의 확대/축소 기능을 제한하는 것은 피해야 합니다. 다음과 같이 변경하세요:
export const viewport: Viewport = { width: 'device-width', initialScale: 1, - maximumScale: 1, - userScalable: false, + maximumScale: 5, + userScalable: true, };이렇게 하면 사용자가 필요에 따라 페이지를 확대할 수 있어 접근성이 향상됩니다.
📝 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.export const viewport: Viewport = { width: 'device-width', initialScale: 1, maximumScale: 5, userScalable: true, };
11-12:
⚠️ Potential issue메타데이터에 뷰포트 설정이 추가되었습니다.
메타데이터 객체에 뷰포트 설정이 추가되었습니다. 이는 나중에 추가된 별도의 뷰포트 설정과 중복될 수 있습니다.
메타데이터 내의 뷰포트 설정과 14-19줄에 있는 별도의 뷰포트 설정이 중복됩니다. Next.js 13 이상에서는 별도의
viewport
객체를 사용하는 것이 권장됩니다. 메타데이터에서 뷰포트 설정을 제거하세요:export const metadata: Metadata = { title: 'Took', description: 'Took name service', - viewport: 'width=device-width, initial-scale=1, maximum-scale=1', };
📝 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.export const metadata: Metadata = { title: 'Took', description: 'Took name service', };
src/features/card-detail/hooks/useReverseScrollView.ts (1)
1-20: 🛠️ Refactor suggestion
메모리 누수와 성능 최적화 문제가 있습니다.
현재 구현은 몇 가지 중요한 문제점이 있습니다:
- 메모리 누수:
useEffect
내에서 이벤트 리스너를 추가하지만, 컴포넌트가 언마운트될 때 이를 제거하는 정리(cleanup) 함수가 없습니다.- 성능 최적화: 이벤트 핸들러 함수가 매 렌더링마다 재생성됩니다.
- 이벤트 처리: wheel 이벤트는 스크롤 중에 매우 자주 발생하여 빈번한 리렌더링을 유발할 수 있습니다.
아래와 같이 수정하는 것을 제안합니다:
-import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; function useReverseScrollView() { const [isReverseScroll, setIsReverseScroll] = useState(false); + const handleWheel = useCallback((event: WheelEvent) => { + if (event.deltaY < 0) { + setIsReverseScroll(true); + } else if (event.deltaY > 0) { + setIsReverseScroll(false); + // 탭바 숨기기 등 처리 + } + }, []); useEffect(() => { - window.addEventListener('wheel', (event) => { - if (event.deltaY < 0) { - setIsReverseScroll(true); - } else if (event.deltaY > 0) { - setIsReverseScroll(false); - // 탭바 숨기기 등 처리 - } - }); + window.addEventListener('wheel', handleWheel); + + return () => { + window.removeEventListener('wheel', handleWheel); + }; - }, []); + }, [handleWheel]); return { isReverseScroll }; } export default useReverseScrollView;또한 성능 최적화를 위해 throttle이나 debounce 기법 적용을 고려해보세요:
import { useEffect, useState, useCallback } from 'react'; import { throttle } from 'lodash'; // lodash 설치 필요 function useReverseScrollView() { const [isReverseScroll, setIsReverseScroll] = useState(false); const handleWheel = useCallback( throttle((event: WheelEvent) => { if (event.deltaY < 0) { setIsReverseScroll(true); } else if (event.deltaY > 0) { setIsReverseScroll(false); // 탭바 숨기기 등 처리 } }, 200), [] ); useEffect(() => { window.addEventListener('wheel', handleWheel); return () => { window.removeEventListener('wheel', handleWheel); handleWheel.cancel(); // throttle된 함수의 pending 호출 취소 }; }, [handleWheel]); return { isReverseScroll }; } export default useReverseScrollView;src/shared/ui/appbar.tsx (1)
69-69:
⚠️ Potential issueAppbar 배경 스타일 로직 수정 필요
배경 스타일을 설정하는 조건부 코드에 문제가 있습니다. 아래 70-80번 라인에 더 상세한 조건부 코드가 있는데, 이 부분이 중복되거나 불완전하게 표시된 것 같습니다.
코드 중복 또는 불완전한 코드가 있는지 확인하고, 아래와 같이 배경 스타일 로직을 수정하는 것이 좋을 것 같습니다:
- className={`z-100 sticky top-0 flex h-16 w-full max-w-[600px] items-center justify-between px-4 py-5 pb-5 pr-4 ${ - page === 'detail' ? 'bg-opacity-90' : page === 'create' ? 'bg-gray-black' : '' - }`} + className={`z-100 sticky top-0 flex h-16 w-full max-w-[600px] items-center justify-between px-4 py-5 pb-5 pr-4 ${ + page === 'detail' && hasBackground + ? 'bg-gray-black bg-opacity-90' + : page === 'detail' + ? 'backdrop-blur-sm' + : page === 'create' + ? 'bg-gray-black' + : '' + }`}📝 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.function Appbar({ page, hasBackground, onLeftClick, onRightClick }: appbarPropsType) { return ( <div className={`z-100 sticky top-0 flex h-16 w-full max-w-[600px] items-center justify-between px-4 py-5 pb-5 pr-4 ${ page === 'detail' && hasBackground ? 'bg-gray-black bg-opacity-90' : page === 'detail' ? 'backdrop-blur-sm' : page === 'create' ? 'bg-gray-black' : '' }`} > {/* ...rest of your component... */} </div> ); }
src/features/card-detail/ui/sns.tsx (1)
26-33: 🛠️ Refactor suggestion
SNS 링크를 클릭 가능하게 만드세요.
현재 SNS 아이콘을 표시하고 있지만, 실제 sns.link로 이동할 수 있는 기능이 없습니다. 사용자가 아이콘을 클릭하여 해당 SNS 페이지로 이동할 수 있도록 하는 것이 좋습니다.
다음과 같이 수정하는 것을 제안합니다:
<div className="flex w-[56px] flex-col items-center" key={i}> - <div - className={`${spacingStyles({ padding: 'md' })} flex items-center justify-center rounded-full bg-opacity-purple-30`} - > - <Image src={snsConfig.iconPath} alt={snsConfig.iconAlt} width={24} height={24} /> - </div> + <a + href={sns.link} + target="_blank" + rel="noopener noreferrer" + className="block" + > + <div + className={`${spacingStyles({ padding: 'md' })} flex items-center justify-center rounded-full bg-opacity-purple-30 cursor-pointer`} + > + <Image src={snsConfig.iconPath} alt={snsConfig.iconAlt} width={24} height={24} /> + </div> + </a> <p className="mt-[7px] w-full truncate text-center text-caption-1">{sns.type}</p> </div>📝 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.<div className="flex w-[56px] flex-col items-center" key={i}> <a href={sns.link} target="_blank" rel="noopener noreferrer" className="block" > <div className={`${spacingStyles({ padding: 'md' })} flex items-center justify-center rounded-full bg-opacity-purple-30 cursor-pointer`} > <Image src={snsConfig.iconPath} alt={snsConfig.iconAlt} width={24} height={24} /> </div> </a> <p className="mt-[7px] w-full truncate text-center text-caption-1">{sns.type}</p> </div>
tailwind.config.ts (1)
141-141: 💡 Verification agent
🧩 Analysis chain
@tailwindcss/line-clamp 플러그인 사용을 재검토하세요.
Tailwind CSS v3.3부터 line-clamp 기능은 이미 Tailwind CSS 코어에 "line-clamp-{n}" 유틸리티로 포함되어 있습니다. 별도의 플러그인은 더 이상 필요하지 않으며 사용을 권장하지 않습니다.
프로젝트에서 사용 중인 Tailwind CSS 버전을 확인하고, 최신 버전을 사용 중이라면 이 플러그인을 제거하는 것이 좋습니다.
🌐 Web query:
Tailwind CSS line-clamp plugin deprecated?
💡 Result:
The
@tailwindcss/line-clamp
plugin is deprecated as of Tailwind CSS v3.3, as line-clamp utilities are now included natively in Tailwind CSS core[4][8]. Here's what you need to know:Migration Steps
- Remove the plugin from your
tailwind.config.js
:// Remove this line: plugins: [require('@tailwindcss/line-clamp')]- Update Tailwind CSS to v3.3 or later:
npm install tailwindcss@latestKey Changes
- Existing
line-clamp-{n}
classes work identically without the plugin[4]- No configuration needed - line-clamp utilities are available by default[4]
- Supported line counts remain 1-6 by default, with configuration options for additional lines[1]
Important Notes:
- The implementation still uses
-webkit-line-clamp
under the hood for cross-browser support[6]- When removing line-clamp, you may need to explicitly reset related properties like
display
andoverflow
[1]- The npm package (@tailwindcss/line-clamp) hasn't been updated since March 2023[1], confirming its deprecated status
For new projects, simply use
line-clamp-{n}
classes directly without installing any additional packages[4]. Existing projects should follow the migration steps to ensure compatibility with future Tailwind CSS updates.Citations:
- 1: https://www.npmjs.com/package/@tailwindcss/line-clamp
- 2: [Feature Proposal]: Add "line-clamp" utility tailwindlabs/tailwindcss#1303
- 3: https://tailwindcss.com/docs/upgrade-guide
- 4: https://tailwindcss.com/blog/tailwindcss-v3-3
- 5: https://tailwindcss.com/docs/upgrading-to-v2
- 6: https://developer.mozilla.org/en-US/docs/Web/CSS/line-clamp
- 7: https://github.com/wahlemedia/tailwindcss-plugin-multiple-lines
- 8: https://awesometailwind.com/tag/deprecated
핵심: Tailwind CSS v3.3 이상에서는 별도의 line-clamp 플러그인을 제거하세요.
Tailwind CSS v3.3부터는
line-clamp-{n}
유틸리티가 코어에 내장되어 있어 별도로@tailwindcss/line-clamp
플러그인을 사용할 필요가 없습니다. 현재tailwind.config.ts
파일의 141번째 줄에서 두 플러그인이 함께 사용되고 있는 것을 확인했습니다.
- 프로젝트에서 Tailwind CSS v3.3 이상을 사용 중이라면, 아래와 같이
@tailwindcss/line-clamp
플러그인을 제거해 주세요.- plugins: [require('tailwindcss-animate'), require('@tailwindcss/line-clamp')], + plugins: [require('tailwindcss-animate')],Tailwind CSS의 최신 버전으로 업그레이드하는 것도 함께 검토해 주시기 바랍니다.
📝 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.plugins: [require('tailwindcss-animate')],
src/features/home/containers/CardContainer.tsx (1)
19-19: 🛠️ Refactor suggestion
데이터 로딩 및 오류 상태를 처리하세요.
useCardQuery
훅을 사용하여 데이터를 가져오고 있지만, 로딩 상태나 오류 상태에 대한 처리가 없습니다. 이로 인해 사용자 경험이 저하될 수 있습니다.다음과 같이 로딩 및 오류 상태를 처리하는 것을 제안합니다:
-const { data } = useCardQuery(); +const { data, isLoading, error } = useCardQuery(); + +if (isLoading) { + return <div className="h-[440px] flex items-center justify-center">카드 정보를 불러오는 중...</div>; +} + +if (error) { + return <div className="h-[440px] flex items-center justify-center">카드 정보를 불러오는데 실패했습니다.</div>; +} + +if (!data || data.length === 0) { + return ( + <Swiper pagination modules={[Pagination]} className="h-[440px]"> + <SwiperSlide> + <AddCard /> + </SwiperSlide> + </Swiper> + ); +}📝 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.const { data, isLoading, error } = useCardQuery(); if (isLoading) { return <div className="h-[440px] flex items-center justify-center">카드 정보를 불러오는 중...</div>; } if (error) { return <div className="h-[440px] flex items-center justify-center">카드 정보를 불러오는데 실패했습니다.</div>; } if (!data || data.length === 0) { return ( <Swiper pagination modules={[Pagination]} className="h-[440px]"> <SwiperSlide> <AddCard /> </SwiperSlide> </Swiper> ); }
src/features/multi-step-form/ui/careerForm/secondStep.tsx (1)
40-42:
⚠️ Potential issue다음 버튼에 onClick 핸들러가 누락되었습니다.
다음 버튼에 onClick 이벤트 핸들러가 없어 실제로 다음 단계로 진행하는 기능이 구현되어 있지 않습니다. 멀티 스텝 폼에서는 단계 간 이동 기능이 필수적입니다.
-<Button className="w-full" disabled={tagCount === 0}> +<Button className="w-full" disabled={tagCount === 0} onClick={() => { + // 다음 단계로 진행하는 로직 구현 +}}> 다음 </Button>📝 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.<Button className="w-full" disabled={tagCount === 0} onClick={() => { // 다음 단계로 진행하는 로직 구현 }}> 다음 </Button>
src/features/card-detail/hooks/query/useCardDetailQuery.ts (2)
11-16: 🛠️ Refactor suggestion
API 호출 시 오류 처리가 없습니다.
getCardDetail
함수에서 API 호출 실패 시 오류 처리가 구현되어 있지 않습니다. API 호출이 실패할 경우 적절한 오류 처리가 필요합니다.const getCardDetail = async (id: string): Promise<CardDetailDto> => { - const data = await client.get<CardDetailDto>(`${CLIENT_SIDE_URL}/api/card/detail`, { - params: { id }, - }); - return data; + try { + const data = await client.get<CardDetailDto>(`${CLIENT_SIDE_URL}/api/card/detail`, { + params: { id }, + }); + return data; + } catch (error) { + console.error('카드 상세 정보를 가져오는 중 오류가 발생했습니다:', error); + throw error; + } };📝 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.const getCardDetail = async (id: string): Promise<CardDetailDto> => { try { const data = await client.get<CardDetailDto>(`${CLIENT_SIDE_URL}/api/card/detail`, { params: { id }, }); return data; } catch (error) { console.error('카드 상세 정보를 가져오는 중 오류가 발생했습니다:', error); throw error; } };
20-26:
⚠️ Potential issue실제 API 응답 대신 항상 목업 데이터를 반환하고 있습니다.
현재 코드는 useQuery를 사용하여 API 요청을 수행하지만, 실제 응답 데이터를 무시하고(
data: _
) 항상 목업 데이터를 반환합니다. 이는 개발 단계에서는 유용할 수 있지만, 프로덕션 환경에서는 적합하지 않습니다. 실제 API 응답을 사용하도록 변경하거나, 환경 변수를 통해 조건부로 목업 데이터를 사용하도록 개선해야 합니다.export const useCardDetailQuery = (id: string) => { - const { data: _ } = useQuery({ + const { data, isLoading, error } = useQuery({ queryKey: ['cardDetail', id], queryFn: () => getCardDetail(id), }); - return { data: mockCardDetailData }; + // 개발 환경에서만 목업 데이터 사용 + if (process.env.NODE_ENV === 'development' && !data) { + return { data: mockCardDetailData, isLoading, error }; + } + + return { data, isLoading, error }; };📝 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.export const useCardDetailQuery = (id: string) => { const { data, isLoading, error } = useQuery({ queryKey: ['cardDetail', id], queryFn: () => getCardDetail(id), }); // 개발 환경에서만 목업 데이터 사용 if (process.env.NODE_ENV === 'development' && !data) { return { data: mockCardDetailData, isLoading, error }; } return { data, isLoading, error }; };
src/features/card-detail/hooks/sample.ts (1)
19-24:
⚠️ Potential issueSNS 링크 배열의 중복된 ID를 수정해주세요.
snsLinks
배열에 중복된 ID('2')가 여러 번 사용되고 있습니다. 이는 React에서 리스트 렌더링 시key
속성에 사용될 경우 문제를 일으킬 수 있습니다. 각 항목에 고유한 ID를 할당해주세요.snsLinks: [ { id: '1', type: 'GitHub', url: 'https://github.com/jaeiny' }, { id: '2', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' }, - { id: '2', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' }, - { id: '2', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' }, - { id: '2', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' }, + { id: '3', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' }, + { id: '4', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' }, + { id: '5', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' }, ],📝 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.snsLinks: [ { id: '1', type: 'GitHub', url: 'https://github.com/jaeiny' }, { id: '2', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' }, { id: '3', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' }, { id: '4', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' }, { id: '5', type: 'LinkedIn', url: 'https://linkedin.com/in/jaeiny' }, ],
src/features/home/hooks/queries/useCardQuery.ts (2)
9-15: 🛠️ Refactor suggestion
_getCard 함수 타입 안전성 개선이 필요합니다.
any
타입 대신 구체적인 타입을 사용하는 것이 좋습니다. API 응답과 파라미터에 인터페이스를 정의하세요.- const _getCard = async (params: any) => { - const data = await client.get<any>('/somewhere/client-side', { + interface CardParams { + userId?: string; + // 기타 필요한 파라미터들 + } + + interface CardResponse { + // API 응답에 맞는 타입 정의 + // 예: id: string, name: string 등 + } + + const _getCard = async (params?: CardParams) => { + const data = await client.get<CardResponse>('/somewhere/client-side', { params, }); return data; };📝 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.interface CardParams { userId?: string; // 기타 필요한 파라미터들 } interface CardResponse { // API 응답에 맞는 타입 정의 // 예: id: string, name: string 등 } const _getCard = async (params?: CardParams) => { const data = await client.get<CardResponse>('/somewhere/client-side', { params, }); return data; };
17-24:
⚠️ Potential issueuseCardQuery에서 모의 데이터 사용에 대한 명확한 표시가 필요합니다.
API 호출 결과를 사용하지 않고 모의 데이터를 반환하는 것이 의도적인지 명확히 해야 합니다. 변수명 '_'도 목적이 불분명합니다.
export const useCardQuery = (params?: any) => { - const { data: _ } = useQuery({ + // TODO: API 연동 완료 후 모의 데이터 대신 실제 데이터 사용하도록 변경 + const { data: apiData } = useQuery({ queryKey: [CARD_QUERY_KEY, params], queryFn: () => _getCard(params), + enabled: false, // API 연동 전까지 쿼리 비활성화 }); return { data: CARD_MOCK }; };📝 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.export const useCardQuery = (params?: any) => { // TODO: API 연동 완료 후 모의 데이터 대신 실제 데이터 사용하도록 변경 const { data: apiData } = useQuery({ queryKey: [CARD_QUERY_KEY, params], queryFn: () => _getCard(params), enabled: false, // API 연동 전까지 쿼리 비활성화 }); return { data: CARD_MOCK }; };
src/features/card-detail/ui/projects.tsx (2)
13-13:
⚠️ Potential issue디버깅용 console.log를 제거하세요.
프로덕션 코드에 console.log가 남아있으면 성능에 영향을 줄 수 있고 불필요한 정보가 노출될 수 있습니다.
- console.log('ProjectsProps :' + data[0].imageUrl);
📝 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.// (The console.log has been removed)
16-17: 🛠️ Refactor suggestion
React 키 값으로 인덱스 사용을 피하세요.
배열 인덱스를 키로 사용하면 항목 순서가 변경될 때 성능 문제가 발생할 수 있습니다. 가능하면 고유 ID를 사용하세요.
- {data.map((project, i) => ( - <div key={i} className="flex flex-col overflow-hidden rounded-md"> + {data.map((project) => ( + <div key={project.id} className="flex flex-col overflow-hidden rounded-md">📝 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.{data.map((project) => ( <div key={project.id} className="flex flex-col overflow-hidden rounded-md">
src/shared/ui/Input/tagInput.tsx (1)
38-53: 🛠️ Refactor suggestion
태그 위치 설정을 개선해주세요.
현재 모든 태그가 동일한 위치(
absolute left-3 top-1
)에 렌더링되어 겹치게 됩니다. 태그들이 시각적으로 구분되도록 위치 지정 방식을 변경하는 것이 좋습니다.<div className="relative flex flex-wrap gap-2 mt-2"> {tags.map((tag, index) => ( <div key={index} - className="flex items-center bg-gray-200 rounded-lg px-2 py-1 absolute left-3 top-1" + className="flex items-center bg-gray-200 rounded-lg px-2 py-1 m-1" > <span className="text-body-5 text-gray-700">{tag}</span> <span className="ml-2 cursor-pointer text-red-500" onClick={() => handleRemoveTag(tag)} > x </span> </div> ))} </div>📝 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.<div className="relative flex flex-wrap gap-2 mt-2"> {tags.map((tag, index) => ( <div key={index} className="flex items-center bg-gray-200 rounded-lg px-2 py-1 m-1" > <span className="text-body-5 text-gray-700">{tag}</span> <span className="ml-2 cursor-pointer text-red-500" onClick={() => handleRemoveTag(tag)} > x </span> </div> ))} </div>
src/features/home/hooks/prefetches/useCardPrefetch.ts (1)
8-10: 🛠️ Refactor suggestion
타입 안전성을 개선하고 실제 API 엔드포인트를 사용해주세요.
현재 함수가 'any' 타입의 매개변수를 사용하고 있고, API 엔드포인트가 '/somewhere/server-side'로 지정되어 있습니다. 구체적인 타입을 정의하고 실제 API 엔드포인트를 지정해주세요.
-const _getCardPrefetch = async (params: any, cookie: ReadonlyRequestCookies) => { - const data = await server.get<any>('/somewhere/server-side', { +// 구체적인 타입 정의 추가 +type CardParams = { + id?: string; + // 필요한 다른 매개변수들 추가 +}; + +type CardResponse = { + // API 응답 타입 정의 + id: string; + // 필요한 다른 필드들 추가 +}; + +const _getCardPrefetch = async (params: CardParams, cookie: ReadonlyRequestCookies) => { + const data = await server.get<CardResponse>('/api/cards', { // 실제 API 엔드포인트 사용📝 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.// 구체적인 타입 정의 추가 type CardParams = { id?: string; // 필요한 다른 매개변수들 추가 }; type CardResponse = { // API 응답 타입 정의 id: string; // 필요한 다른 필드들 추가 }; const _getCardPrefetch = async (params: CardParams, cookie: ReadonlyRequestCookies) => { const data = await server.get<CardResponse>('/api/cards', { // 실제 API 엔드포인트 사용 params, }); // ... (rest of the function implementation) };
src/features/card-detail/ui/cardDetailHeader.tsx (2)
12-16: 🛠️ Refactor suggestion
데이터 로딩 및 오류 처리 필요
현재 코드는 데이터 로딩 상태나 오류 상태를 처리하지 않고 있습니다. 이로 인해 데이터가 없는 경우 런타임 오류가 발생할 수 있습니다.
useCardDetailQuery
에서isLoading
,error
상태도 함께 가져와서 처리하는 것을 권장합니다:- const { data } = useCardDetailQuery(id as string); + const { data, isLoading, error } = useCardDetailQuery(id as string); + + if (isLoading) { + return <div className="w-full h-[300px] flex items-center justify-center">로딩 중...</div>; + } + + if (error || !data) { + return <div className="w-full h-[300px] flex items-center justify-center">데이터를 불러오는 데 실패했습니다.</div>; + }📝 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.const CardDetailHeader = () => { const { id } = useParams(); const { data, isLoading, error } = useCardDetailQuery(id as string); if (isLoading) { return <div className="w-full h-[300px] flex items-center justify-center">로딩 중...</div>; } if (error || !data) { return <div className="w-full h-[300px] flex items-center justify-center">데이터를 불러오는 데 실패했습니다.</div>; } const userJob = (data?.data?.job as JobType) || 'DEVELOPER'; // ... rest of the component code }
44-60:
⚠️ Potential issue옵셔널 체이닝 사용 불일치
코드 전체에서 옵셔널 체이닝(
?.
)의 사용이 일관되지 않습니다. 일부는data?.data
로 사용하고 일부는data.data
로 사용하여 잠재적인 null 참조 오류가 발생할 수 있습니다.다음과 같이 모든 데이터 접근에서 일관된 옵셔널 체이닝을 사용하세요:
- <p className="title-1 line-clamp-1">{data.data.nickname}</p> + <p className="title-1 line-clamp-1">{data?.data?.nickname}</p> <div className={`${spacingStyles({ marginBottom: 'ms' })} line-clamp-1 flex w-full items-center text-title-3`} > <span className={`max-w-1/2 truncate ${spacingStyles({ marginRight: 'sm' })}`}> - {data?.data.detailJob} + {data?.data?.detailJob} </span> {data?.data && ( <> <span className="flex-shrink-0 px-1">|</span> <span className={`max-w-1/2 truncate ${spacingStyles({ marginLeft: 'sm' })}`}> - {data?.data.organization} + {data?.data?.organization} </span> </> )} </div> - <p className="body-5 line-clamp-2">{data?.data.summary}</p> + <p className="body-5 line-clamp-2">{data?.data?.summary}</p>📝 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.<p className="title-1 line-clamp-1">{data?.data?.nickname}</p> <div className={`${spacingStyles({ marginBottom: 'ms' })} line-clamp-1 flex w-full items-center text-title-3`} > <span className={`max-w-1/2 truncate ${spacingStyles({ marginRight: 'sm' })}`}> {data?.data?.detailJob} </span> {data?.data && ( <> <span className="flex-shrink-0 px-1">|</span> <span className={`max-w-1/2 truncate ${spacingStyles({ marginLeft: 'sm' })}`}> {data?.data?.organization} </span> </> )} </div> <p className="body-5 line-clamp-2">{data?.data?.summary}</p>
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.
Caution
Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
🛑 Comments failed to post (27)
src/features/home/types/index.ts (1)
1-1: 💡 Verification agent
🧩 Analysis chain
타입 이름에 오타가 있습니다.
JopType
이라는 타입 이름에 오타가 있습니다. 올바른 단어는JobType
입니다.-export type JopType = 'designer' | 'developer'; +export type JobType = 'designer' | 'developer';이 타입을 사용하는 모든 파일에서도 수정이 필요할 수 있습니다.
🏁 Script executed:
#!/bin/bash # JopType 타입을 사용하는 모든 파일 찾기 rg "JopType" --type tsLength of output: 798
타입 이름 오타 수정 필요 –
JopType
에서JobType
으로 모든 변경 적용
src/features/home/types/index.ts
파일 내 타입 이름에 오타가 있습니다. 아래와 같이 수정해야 합니다:-export type JopType = 'designer' | 'developer'; +export type JobType = 'designer' | 'developer';추가로, 이 타입을 사용하는 다른 파일들에서도 아래와 같이
JopType
을JobType
으로 변경해야 합니다:
- src/features/home/mocks/index.ts
- import 구문과 타입 지정 부분에서 변경 필요
- src/features/home/components/BusinessCard/CardTag.tsx
- import 및 사용 부분 변경 필요
- src/features/home/components/BusinessCard/Card.tsx
- import 및 타입 사용 부분 변경 필요
해당 변경 사항을 전역적으로 적용하여 타입 불일치 문제를 해결해 주세요.
📝 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.export type JobType = 'designer' | 'developer';
src/features/home/components/BusinessCard/Background/AddCradBackground.tsx (1)
1-3:
⚠️ Potential issue파일명에 오타가 있습니다.
파일명이
AddCradBackground.tsx
로 되어 있지만, 컴포넌트 이름은AddCardBackground
입니다.
파일명을AddCardBackground.tsx
로 수정하는 것이 좋겠습니다.src/features/home/providers/CardSwiperProvider.tsx (1)
6-8: 🛠️ Refactor suggestion
Swiper 컴포넌트 래퍼가 필요합니다만 구성 옵션이 부족합니다.
현재 구현은 기본적인 Swiper 래핑만 제공하고 있어 유연성이 제한됩니다. 카드 디테일 페이지 구현을 위해 더 많은 설정이 필요할 수 있습니다.
다음과 같이 Swiper 구성 옵션을 추가하여 컴포넌트를 개선해보세요:
'use client'; import { PropsWithChildren } from 'react'; -import { Swiper } from 'swiper/react'; +import { Swiper, SwiperProps } from 'swiper/react'; +import { Pagination } from 'swiper/modules'; +import 'swiper/css'; +import 'swiper/css/pagination'; -export const CardSwiperProvider = ({ children }: PropsWithChildren) => { +export const CardSwiperProvider = ({ + children, + swiperOptions = {} +}: PropsWithChildren<{ swiperOptions?: SwiperProps }>) => { return ( - <Swiper>{children}</Swiper> + <Swiper + modules={[Pagination]} + spaceBetween={20} + slidesPerView={'auto'} + pagination={{ clickable: true }} + centeredSlides={true} + {...swiperOptions} + > + {children} + </Swiper> ); };📝 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.'use client'; import { PropsWithChildren } from 'react'; import { Swiper, SwiperProps } from 'swiper/react'; import { Pagination } from 'swiper/modules'; import 'swiper/css'; import 'swiper/css/pagination'; export const CardSwiperProvider = ({ children, swiperOptions = {} }: PropsWithChildren<{ swiperOptions?: SwiperProps }>) => { return ( <Swiper modules={[Pagination]} spaceBetween={20} slidesPerView={'auto'} pagination={{ clickable: true }} centeredSlides={true} {...swiperOptions} > {children} </Swiper> ); };
src/app/layout.tsx (1)
11-19: 💡 Verification agent
🧩 Analysis chain
viewport 설정 접근성 문제 확인 필요
viewport 설정에
maximumScale: 1
과userScalable: false
속성이 추가되었습니다. 이 설정은 사용자가 페이지를 확대할 수 없게 만들어 일관된 디자인을 유지할 수 있지만, 시각적 장애가 있는 사용자들의 접근성에 문제를 일으킬 수 있습니다.웹 접근성 지침(WCAG)에 따르면 사용자가 화면을 확대할 수 있어야 합니다. 따라서 이 설정을 재고하는 것이 좋습니다.
다음과 같이 수정하는 것을 권장합니다:
export const viewport: Viewport = { width: 'device-width', initialScale: 1, - maximumScale: 1, - userScalable: false, + maximumScale: 5, + userScalable: true, };
🌐 Web query:
웹 접근성 지침 WCAG에서 viewport maximumScale과 userScalable 설정에 대해 권장하는 사항은?
💡 Result:
웹 접근성 지침(WCAG)은 뷰포트의
maximum-scale
과user-scalable
설정에 대해 명확한 권장 사항을 제시합니다. 이는 저시력 사용자가 텍스트와 콘텐츠를 확대하여 사용할 수 있도록 보장하기 위한 것입니다.주요 권장사항
`user-scalable="no" 사용 금지
- 이 설정은 사용자의 화면 확대/축소 기능을 완전히 비활성화합니다. WCAG 1.4.4(텍스트 크기 조정) 및 1.4.10(리플로우)에 따라 사용자가 콘텐츠를 최대 200%까지 확대할 수 있어야 합니다[1][3][7].
- iOS 10 이후부터는
user-scalable="no"
를 무시하도록 변경되었으나, 접근성 문제로 인해 해당 설정 자체를 피해야 합니다[2].
maximum-scale
값은 최소 2.0 이상 설정
maximum-scale=1.0
또는 그 이하로 설정하면 확대가 제한되어 WCAG 1.4.4를 위반합니다. 최소 2.0 이상으로 설정해야 200% 확대가 가능합니다[3][7][9].- 일부 브라우저는
maximum-scale
을 생략할 경우 기본값으로 높은 확대 수준을 허용하므로, 명시적으로maximum-scale=2.0
을 권장합니다[3][13].
WCAG 기준과의 연관성
- 1.4.4 Resize Text (AA): 텍스트를 200%까지 확대할 때 콘텐츠 손실 없이 조정 가능해야 합니다[5][13].
- 1.4.10 Reflow (AA): 400% 확대 시 수평 스크롤 없이 콘텐츠가 단일 열로 재배치되어야 합니다[6][11].
잘못된 설정 예시 및 해결 방안
<!-- 문제 있는 설정 --> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <!-- 권장 설정 --> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=2.0">
user-scalable="no"
제거 및maximum-scale=2.0
으로 변경하면 WCAG를 준수할 수 있습니다[1][7][9].
배경 및 중요성
- 저시력 사용자 지원: 화면 확대는 시각 장애인에게 필수적이며, 제한 시 웹 이용이 어려워집니다[1][2][7].
- 법적 준수: 유럽 및 미국 등에서는 WCAG 준수가 법적 의무화되고 있습니다[8][9].
- 반응형 디자인: 뷰포트 설정은 모바일 기기에서의 접근성과 직접적인 연관이 있습니다[6][11].
이러한 지침은 사용자 경험을 개선하고 법적 위험을 줄이는 데 필수적입니다.
Citations:
- 1: https://www.accessibilitychecker.org/wcag-guides/ensure-can-scale-a-significant-amount/
- 2: https://mulder21c.io/lets-work-on-why-please/
- 3: https://www.w3.org/WAI/standards-guidelines/act/rules/b4f0c3/
- 4: https://nuli.navercorp.com/data/blog/NULI_Web%20Content%20Accessibility%20Guidelines_2_1_KR.pdf
- 5: https://yatil.net/blog/resize-text-reflow
- 6: https://a11ykr.github.io/wcag22/
- 7: https://dequeuniversity.com/rules/axe/4.4/meta-viewport
- 8: https://developer.mozilla.org/ko/docs/Web/Accessibility/Understanding_WCAG
- 9: https://rocketvalidator.com/accessibility-validation/axe/4.7/meta-viewport
- 10: https://blog.naver.com/mongnme/220632641981?viewType=pc
- 11: https://nuli.navercorp.com/data/blog/NULI_Understanding_New_Features-_KR.pdf
- 12: http://www.kwacc.or.kr/WAI/wcag21/
- 13: https://www.w3.org/TR/WCAG21/
- 14: https://hsr.ca.gov/ko/accessibility/
- 15: https://www.wcag.com/resource/what-is-wcag/
- 16: Is there an intended minimum viewport size for 1.4.4? w3c/wcag#2101
뷰포트 확대 제한 수정 필요
현재
viewport
설정은maximumScale: 1
과userScalable: false
로 되어 있어, 사용자가 콘텐츠를 확대할 수 없으므로 WCAG 접근성 기준(최소 2.0 이상의 확대 지원)에 부합하지 않습니다. 이에 따라 아래와 같이 수정해 주세요:export const viewport: Viewport = { width: 'device-width', initialScale: 1, - maximumScale: 1, - userScalable: false, + maximumScale: 2, + userScalable: true, };이 수정은 저시력 사용자가 페이지를 최대 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.viewport: 'width=device-width, initial-scale=1, maximum-scale=1', }; export const viewport: Viewport = { width: 'device-width', initialScale: 1, maximumScale: 2, userScalable: true, };
src/features/card-detail/ui/snsLink.tsx (3)
15-25: 🛠️ Refactor suggestion
SNS 링크 렌더링 로직에 개선이 필요합니다.
현재 구현에서는 모든 SNS 링크에 동일한 디자인 아이콘이 사용되고 있습니다. 각 SNS 유형에 따라 다른 아이콘을 표시하는 것이 더 적절해 보입니다.
다음과 같이 수정하는 것을 제안합니다:
{data.map((e, i) => { return ( <div className="flex flex-col items-center" key={i}> <div className="flex h-[56px] w-[56px] items-center justify-center rounded-full bg-opacity-purple-30"> - <Image src="/icons/design-icon-white.svg" alt="디자인 로고" width={28} height={28} /> + <Image + src={`/icons/${e.type.toLowerCase()}-icon.svg`} + alt={`${e.type} 아이콘`} + width={28} + height={28} + /> </div> <span>{e.type}</span> </div> ); })}📝 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.{data.map((e, i) => { return ( <div className="flex flex-col items-center" key={i}> <div className="flex h-[56px] w-[56px] items-center justify-center rounded-full bg-opacity-purple-30"> - <Image src="/icons/design-icon-white.svg" alt="디자인 로고" width={28} height={28} /> + <Image + src={`/icons/${e.type.toLowerCase()}-icon.svg`} + alt={`${e.type} 아이콘`} + width={28} + height={28} + /> </div> <span>{e.type}</span> </div> ); })}
17-24: 🛠️ Refactor suggestion
SNS 링크 클릭 기능 추가 필요
현재 구현에서는 SNS 링크를 보여주기만 하고 실제로 클릭 가능한 링크 기능이 없습니다. 사용자가 SNS 프로필로 바로 이동할 수 있도록 링크 기능을 추가하는 것이 좋겠습니다.
<div className="flex flex-col items-center" key={i}> + <a + href={e.url} + target="_blank" + rel="noopener noreferrer" + className="flex flex-col items-center" + > <div className="flex h-[56px] w-[56px] items-center justify-center rounded-full bg-opacity-purple-30"> <Image src="/icons/design-icon-white.svg" alt="디자인 로고" width={28} height={28} /> </div> <span>{e.type}</span> + </a> </div>📝 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.<div className="flex flex-col items-center" key={i}> <a href={e.url} target="_blank" rel="noopener noreferrer" className="flex flex-col items-center" > <div className="flex h-[56px] w-[56px] items-center justify-center rounded-full bg-opacity-purple-30"> <Image src="/icons/design-icon-white.svg" alt="디자인 로고" width={28} height={28} /> </div> <span>{e.type}</span> </a> </div>
6-6: 💡 Verification agent
🧩 Analysis chain
타입 정의의 출처 확인 필요
SNSLinkDto
타입을 현재 'sample' 파일에서 가져오고 있습니다. 이는 실제 프로덕션 코드에서는 적절하지 않을 수 있습니다. 실제 API 응답 타입을 사용하는 것이 좋겠습니다.
🌐 Web query:
Next.js 프로젝트에서 API 응답 타입 관리 모범 사례
💡 Result:
Next.js 프로젝트에서 API 응답 타입을 효과적으로 관리하기 위한 모범 사례는 다음과 같습니다:
1. 전역 타입 정의 활용
- 공통 응답 구조 표준화:
.d.ts
파일에 기본 응답 인터페이스를 정의해 프로젝트 전체에서 일관된 구조 사용[6][9]// api.d.ts declare interface ApiResponse<T> { data: T; status: number; message: string; }
- 환경 변수 타입 안전성 강화:
ProcessEnv
인터페이스 확장으로 환경 변수 접근 시 타입 검증[6][11]declare namespace NodeJS { interface ProcessEnv { NEXT_PUBLIC_API_URL: string; DATABASE_URL: string; } }2. Next.js 기본 타입 시스템 확장
- API 핸들러 타입 명시:
NextApiResponse
제네릭으로 응답 구조 강제화[1][10]import type { NextApiRequest, NextApiResponse } from 'next'; type UserData = { id: number; name: string; }; export default function handler( req: NextApiRequest, res: NextApiResponse<ApiResponse<UserData[]>> ) { // ... }
- 미들웨어 응답 확장: 커스텀 응답 타입으로 동적 파라미처 처리[3][17]
interface CustomResponse extends NextResponse { params?: { action: string }; }3. 자동화 도구 통합
- OpenAPI 기반 코드 생성: Orval을 이용해 스키마에서 타입 자동 생성[9]
// orval.config.ts export default defineConfig({ api: { input: './openapi.yaml', output: './src/api/generated' } });
- React Query 통합: 생성된 타입을 쿼리 훅에 자동 적용[9][14]
const { data } = useGetApiTodos(); // data는 자동으로 ApiResponse<Todo[]> 타입 추론4. 에러 처리 표준화
- 일관된 에러 응답 구조: HTTP 상태 코드와 연동된 에러 타입 정의[7][12]
interface ErrorResponse { errorCode: string; message: string; details?: Record<string, unknown>; }
- 타입 가드 구현: 런타임 유효성 검사 도구(zod 등)와 결합[3][5]
function isErrorResponse(res: unknown): res is ErrorResponse { return typeof res === 'object' && res !== null && 'errorCode' in res; }5. 모듈화 전략
- 도메인 주도 타입 분리: 기능별 독립적인 타입 모듈 구성[6][15]
/src /types /auth.ts /product.ts /common.ts
- API 계층 타입 격리: 프론트엔드와 백엔드 타입 의존성 분리[9][11]
// 백엔드 전용 타입 declare module "next/server" { interface NextResponse { customBackendProp: string; } }이러한 접근 방식은 타입 안전성과 개발 생산성 간의 균형을 유지하면서 다음 장점을 제공합니다:
- API 변경 시 컴파일 단계에서 오류 감지
- 자동 완성 기능을 통한 개발 경험 개선
- 문서화 없이도 타입 정의를 통해 API 스펙 이해 가능
- 프론트엔드/백엔드 팀 간 커뮤니케이션 효율화
특히 Next.js 14 이상 버전에서는 App Router와 Server Actions 도입으로 인해 타입 관리의 중요성이 더욱 강조되고 있으며[13][17], OpenAPI 기반의 자동화 도구 활용이 프로젝트 규모 확장 시 필수 요소로 부상하고 있습니다[9][14].
Citations:
- 1: https://velog.io/@codns1223/Nextjs-API-Routes
- 2: https://apidog.com/kr/blog/nextjs-fetch-data-2/
- 3: https://www.tempmail.us.com/ko/nextjs/vercel-%EB%B0%B0%ED%8F%AC-%EC%8B%9C-next-js%EC%9D%98-typescript-api-%EA%B2%BD%EB%A1%9C-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0
- 4: https://reactnext-central.xyz/blog/nextjs/middleware
- 5: https://apidog.com/kr/blog/next-js-api-example-3/
- 6: https://daddydontsleep.tistory.com/146
- 7: https://apidog.com/kr/blog/next-js-call-api-2/
- 8: https://velog.io/@yoosion030/Next.js-14
- 9: https://velog.io/@ph8nt0m/%ED%83%80%EC%9E%85-%EC%95%88%EC%A0%84-API-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-NextJS%EC%97%90%EC%84%9C-OpenAPI%EC%99%80-Orval-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0
- 10: https://nextjs.org/docs/pages/building-your-application/routing/api-routes
- 11: https://recodelog.com/blog/next/environment
- 12: https://apidog.com/kr/blog/next-js-post-request-2/
- 13: https://www.heropy.dev/p/n7JHmI
- 14: https://velog.io/@gazero_/Next-JS%EC%97%90%EC%84%9C-react-query-%EC%A0%81%EC%9A%A9%EA%B8%B0-1%ED%83%84
- 15: https://pozafly.github.io/nextjs/about-modularizing-api-routes-code-in-nextjs/
- 16: https://reactnext-central.xyz/blog/nextjs/performance-nextjs-script-optimization
- 17: https://velog.io/@mjieun/Next.js-APIs-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0
- 18: https://docs.datadoghq.com/ko/real_user_monitoring/guide/monitor-your-nextjs-app-with-rum/
- 19: https://je-developing.tistory.com/335
- 20: https://handhand.tistory.com/280
SNSLinkDto 타입 정의 업데이트 필요
- 현재 SNSLinkDto가 샘플 파일에서 가져온 타입으로 설정되어 있습니다. 이 방식은 프로덕션에서 실제 API 응답에 맞지 않을 수 있습니다.
- Next.js 모범 사례에 따르면, API 응답 타입은 전역 타입 정의나 도메인별 분리된 파일(예:
/src/types/apiResponses.d.ts
)을 활용하여 프로젝트 전반에 일관되게 관리하는 것이 좋습니다.- 실제 API 응답에 기반한 타입 정의로 변경하여 API 변경 시 컴파일 단계에서 오류를 미리 감지할 수 있도록 수정해 주세요.
src/features/home/components/BusinessCard/CardTag.tsx (2)
4-4:
⚠️ Potential issueJopType 오타 수정 필요
import 문에 'JopType'이라는 이름이 사용되었는데, 이는 'JobType'의 오타로 보입니다. 타입 이름을 올바르게 수정해주세요.
-import { JopType } from '@/features/home/types'; +import { JobType } from '@/features/home/types';📝 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.import { JobType } from '@/features/home/types';
9-11:
⚠️ Potential issueProps 타입 정의 수정 필요
JopType 대신 JobType을 사용하도록 Props 타입 정의를 수정해주세요.
type Props = { - tagType: JopType; + tagType: JobType; } & HTMLAttributes<HTMLDivElement>;📝 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.type Props = { tagType: JobType; } & HTMLAttributes<HTMLDivElement>;
src/features/card-detail/mocks/sample.ts (1)
17-38:
⚠️ Potential issueSNS 링크 중복 및 잘못된 URL 확인 필요
SNS 목록에 다음과 같은 문제가 있습니다:
- 인스타그램 링크가 GitHub URL로 설정되어 있습니다.
- 네이버 블로그가 중복으로 등록되어 있습니다.
- 모든 SNS의 링크가 동일한 GitHub URL로 설정되어 있습니다.
각 SNS 타입에 맞는 적절한 URL로 수정하고 중복을 제거해주세요.
sns: [ { type: 'INSTAGRAM', - link: 'https://github.com/jaeiny', + link: 'https://instagram.com/jaeiny', }, { type: 'GITHUB', link: 'https://github.com/jaeiny', }, { type: 'LINKEDIN', - link: 'https://github.com/jaeiny', + link: 'https://linkedin.com/in/jaeiny', }, { type: 'NAVER_BLOG', - link: 'https://github.com/jaeiny', + link: 'https://blog.naver.com/jaeiny', }, - { - type: 'NAVER_BLOG', - link: 'https://github.com/jaeiny', - }, ],📝 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.sns: [ { type: 'INSTAGRAM', link: 'https://instagram.com/jaeiny', }, { type: 'GITHUB', link: 'https://github.com/jaeiny', }, { type: 'LINKEDIN', link: 'https://linkedin.com/in/jaeiny', }, { type: 'NAVER_BLOG', link: 'https://blog.naver.com/jaeiny', }, ],
src/features/home/containers/CardContainer.tsx (1)
18-20:
⚠️ Potential issueuseCardQuery 데이터 로딩 상태 처리 필요
현재 코드는 데이터가 항상 있다고 가정하고 있습니다. 실제 API 호출 시 로딩 상태나 에러 상태에 대한 처리가 필요합니다.
export const CardContainer = () => { - const { data } = useCardQuery(); + const { data, isLoading, error } = useCardQuery(); + + if (isLoading) { + return <div className="h-[440px] flex items-center justify-center">로딩 중...</div>; + } + + if (error || !data) { + return <div className="h-[440px] flex items-center justify-center">데이터를 불러오는데 실패했습니다.</div>; + } return (📝 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.export const CardContainer = () => { const { data, isLoading, error } = useCardQuery(); if (isLoading) { return <div className="h-[440px] flex items-center justify-center">로딩 중...</div>; } if (error || !data) { return <div className="h-[440px] flex items-center justify-center">데이터를 불러오는데 실패했습니다.</div>; } return ( // ... rest of the component JSX ); };
src/features/home/hooks/queries/useCardQuery.ts (2)
17-24:
⚠️ Potential issue실제 API 응답 대신 목업 데이터 사용 중
현재 이 훅은 실제 API 호출 결과를 무시하고 항상
CARD_MOCK
목업 데이터를 반환합니다. 개발 완료 시 실제 API 응답을 사용하도록 수정이 필요합니다. 또한 에러 핸들링 로직이 없어 API 호출 실패 시 대응 방안이 없습니다.export const useCardQuery = (params?: any) => { - const { data: _ } = useQuery({ + const { data, error, isLoading } = useQuery({ queryKey: [CARD_QUERY_KEY, params], queryFn: () => _getCard(params), }); - return { data: CARD_MOCK }; + // 개발 단계에서만 목업 데이터 사용 + return { + data: process.env.NODE_ENV === 'development' ? CARD_MOCK : data, + error, + isLoading + }; };📝 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.export const useCardQuery = (params?: any) => { const { data, error, isLoading } = useQuery({ queryKey: [CARD_QUERY_KEY, params], queryFn: () => _getCard(params), }); // 개발 단계에서만 목업 데이터 사용 return { data: process.env.NODE_ENV === 'development' ? CARD_MOCK : data, error, isLoading }; };
9-15: 💡 Verification agent
🧩 Analysis chain
API 엔드포인트 하드코딩 및 타입 개선 필요
'/somewhere/client-side' 엔드포인트는 플레이스홀더로 보입니다. 실제 API 엔드포인트로 변경이 필요하며,
params: any
타입도 더 구체적인 타입으로 정의하면 좋겠습니다.
🏁 Script executed:
#!/bin/bash # API 엔드포인트 상수가 정의되어 있는지 확인 echo "API 엔드포인트 상수 검색 결과:" rg -i "const.*url|CLIENT_SIDE_URL" --type tsLength of output: 1975
API 엔드포인트 상수 사용 및 파라미터 타입 정의 필요
src/features/home/hooks/queries/useCardQuery.ts
파일에서 현재 하드코딩된'/somewhere/client-side'
엔드포인트는 플레이스홀더로 보입니다.
→ 요청:src/shared/constants/index.ts
에 정의된CLIENT_SIDE_URL
혹은 실제 API 엔드포인트 상수를 사용해 주세요.params: any
로 정의된 부분은 임시로 사용한 타입으로 보입니다.
→ 요청: 해당 API의 요청 파라미터에 맞는 구체적인 타입(예: interface)을 정의하여 사용해 주세요.src/features/card-detail/hooks/query/useCardDetailQuery.ts (1)
19-26: 🛠️ Refactor suggestion
목업 데이터 사용 및 환경 분리 필요
useCardDetailQuery
훅도 실제 API 응답을 무시하고 항상 목업 데이터를 반환합니다. 개발/프로덕션 환경에 따라 분리하고, 로딩 상태와 에러 상태도 반환하는 것이 좋겠습니다.export const useCardDetailQuery = (id: string) => { - const { data: _ } = useQuery({ + const { data, error, isLoading } = useQuery({ queryKey: ['cardDetail', id], queryFn: () => getCardDetail(id), }); - return { data: mockCardDetailData }; + return { + data: process.env.NODE_ENV === 'development' ? mockCardDetailData : data, + isLoading, + error + }; };📝 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.export const useCardDetailQuery = (id: string) => { const { data, error, isLoading } = useQuery({ queryKey: ['cardDetail', id], queryFn: () => getCardDetail(id), }); return { data: process.env.NODE_ENV === 'development' ? mockCardDetailData : data, isLoading, error }; };
src/features/card-detail/config/sns-config.ts (1)
29-32: 💡 Verification agent
🧩 Analysis chain
누락된 SNS 아이콘 및 임시 아이콘 사용
일부 SNS 아이콘(Facebook, Medium, ETC)이 누락되어 있고 임시로 'design-icon-white.svg'를 사용하고 있습니다. 실제 아이콘으로 업데이트가 필요합니다.
Also applies to: 58-61, 67-70
🏁 Script executed:
#!/bin/bash # 실제 아이콘 파일 존재 여부 확인 echo "SNS 아이콘 파일 존재 여부 확인:" fd -t f "facebook|medium|etc" --exec file {} \; public/icons/sns-icon/ echo "현재 사용 중인 임시 아이콘 파일 확인:" file public/icons/design-icon-white.svg 2>/dev/null || echo "임시 아이콘 파일이 없습니다."Length of output: 266
확인 결과: SNS 아이콘 관련 파일 누락 문제
- 파일:
src/features/card-detail/config/sns-config.ts
(라인 29-32, 58-61, 67-70)- 이슈:
- 설정에서 Facebook, Medium, ETC 등 일부 SNS 아이콘에 대해 임시로
design-icon-white.svg
경로를 사용하고 있으나, 해당 임시 아이콘 파일이 존재하지 않는 것으로 확인되었습니다.public/icons/sns-icon/
경로 내에서 실제 SNS 아이콘 파일(Facebook, Medium, ETC)도 찾을 수 없습니다.- 요청 사항:
- 누락된 실제 SNS 아이콘 파일을
/public/icons/sns-icon/
디렉토리에 추가하거나, 올바른 경로의 아이콘 파일로 업데이트해 주세요.src/features/card-detail/ui/cardDetailHeader.tsx (1)
12-15: 🛠️ Refactor suggestion
데이터 페칭 로직 개선 필요
현재 샘플 훅을 사용하여 데이터를 가져오고 있으나, 로딩 상태나 에러 처리가 없습니다. 실제 API와 통합할 때는 적절한 상태 처리가 필요합니다.
const CardDetailHeader = () => { - const { data: userData } = useUser(); - const { data: userDataDetail } = useUserDetail(); + const { data, isLoading, error } = useCardDetailQuery(); + + if (isLoading) return <div className="w-full h-[200px] flex items-center justify-center">로딩 중...</div>; + if (error) return <div className="w-full h-[200px] flex items-center justify-center">에러가 발생했습니다</div>; + if (!data) return null; + + const { name, jobTitle, organization, summary, region } = data;Committable suggestion skipped: line range outside the PR's diff.
package.json (1)
2-2: 💡 Verification agent
🧩 Analysis chain
프로젝트 이름 변경 확인
프로젝트 이름이 "Took-FE"에서 "took-fe"로 변경되었습니다. 이 변경이 의도적인지, 그리고 다른 설정 파일(예: README, CI/CD 설정 등)에서도 일관되게 적용되었는지 확인해주세요.
🏁 Script executed:
#!/bin/bash # 프로젝트 이름이 다른 파일에서도 일관되게 사용되는지 확인 echo "README.md에서 프로젝트 이름 확인:" rg -i "took-?fe" README.md || echo "README.md에서 프로젝트 이름 언급 없음" echo "\n.github 폴더 내 워크플로우 파일에서 프로젝트 이름 확인:" rg -i "took-?fe" .github/ || echo ".github 폴더에서 프로젝트 이름 언급 없음"Length of output: 525
프로젝트 이름 불일치 문제 – 수정 필요
package.json
에서는 프로젝트 이름이"took-fe"
로 변경된 것으로 보입니다.README.md
에는 프로젝트 이름이 언급되지 않아, 업데이트가 누락된 것 같습니다..github/workflows
내 워크플로우 파일(pr-comment.yml
,deploy-integration.yml
)에서는 여전히"Took-FE"
및 관련 명칭(Took-FE-develop
,Took-FE-production
)이 사용되고 있습니다.위 사항들에 대해 프로젝트 이름을 일관되게 통일할 수 있도록 확인 및 수정 부탁드립니다.
src/features/multi-step-form/ui/careerForm/secondStep.tsx (2)
40-42:
⚠️ Potential issueNext 버튼에 onClick 핸들러가 없습니다
다음 단계로 이동하는 버튼에 onClick 핸들러가 구현되어 있지 않습니다. 멀티스텝 폼에서 다음 단계로 진행하기 위한 핸들러를 추가해야 합니다.
- <Button className="w-full" disabled={tagCount === 0}> + <Button className="w-full" disabled={tagCount === 0} onClick={handleNextStep}> 다음 </Button>다음 단계로 이동하는 핸들러 함수를 구현하고 추가하세요.
📝 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.<Button className="w-full" disabled={tagCount === 0} onClick={handleNextStep}> 다음 </Button>
22-22: 💡 Verification agent
🧩 Analysis chain
TagBox에 선택된 태그를 처리하는 로직 확인 필요
TagBox 컴포넌트에 tagCount, tagArray, setTagCount, setTagArray를 props로 전달하고 있지만, 이 값들이 실제로 어떻게 사용되는지 확인할 수 없습니다. 태그 선택 시 다음 단계로 이 데이터를 전달하는 로직이 필요합니다.
🏁 Script executed:
#!/bin/bash # TagBox 컴포넌트 구현을 확인 find . -name "TagBox.tsx" -o -name "TagBox.jsx" | xargs catLength of output: 1798
태그 선택 데이터 전달 로직 개선 요청
TagBox 컴포넌트에서는 클릭 시 선택된 태그에 대해 상태(tagCount, tagArray)의 추가/제거 처리가 올바르게 수행되고 있음이 확인되었습니다. 다만, 이 상태값이 이후 프로세스(예: 제출 또는 다음 단계로의 데이터 전달)에 활용되는 로직은 확인되지 않습니다.
- 현재 TagBox는 선택된 태그를 갱신한 후 콘솔에 출력하는 용도로만 사용되고 있으며, 이후 단계에서 해당 데이터를 활용하는 로직이 누락된 것으로 보입니다.
- 선택된 태그와 관련 상태 값을 후속 단계나 상위 컴포넌트로 전달하여 처리하는 로직이 추가 구현되어야 합니다.
src/features/card-detail/ui/projects.tsx (1)
16-17: 🛠️ Refactor suggestion
React 컴포넌트 리스트의 key로 인덱스 대신 고유 식별자를 사용하세요.
React에서 배열을 매핑할 때 인덱스를 key로 사용하는 것은 권장되지 않습니다. 프로젝트 목록의 순서가 변경되거나 항목이 추가/제거되는 경우 렌더링 문제가 발생할 수 있습니다. 가능하면 각 프로젝트의 고유 ID를 key로 사용하는 것이 좋습니다.
- {data.map((project, i) => ( - <div key={i} className="flex flex-col overflow-hidden rounded-md"> + {data.map((project) => ( + <div key={project.id} className="flex flex-col overflow-hidden rounded-md">📝 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.{data.map((project) => ( <div key={project.id} className="flex flex-col overflow-hidden rounded-md">
src/shared/ui/Input/tagInput.tsx (2)
17-26:
⚠️ Potential issue주석 처리된 코드 및 콘솔 로그 정리 필요
주석 처리된 코드와 콘솔 로그는 프로덕션 코드에서 제거하거나 구현을 완료해야 합니다. 현재 상태로는 handleKeyDown 함수가 실제로 태그를 추가하는 기능을 수행하지 않습니다.
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === 'Enter' || e.key === ' ') { - console.log(tags, 'tags'); e.preventDefault(); - // const trimmedValue = inputValue.trim(); - // if (trimmedValue && !tags.includes(trimmedValue)) { - // setTags((prevTags) => [...prevTags, trimmedValue]); - // setInputValue(''); // 태그 추가 후 인풋 초기화 - // } + const trimmedValue = inputValue.trim(); + if (trimmedValue && !tags.includes(trimmedValue)) { + setTags((prevTags) => [...prevTags, trimmedValue]); + setInputValue(''); // 태그 추가 후 인풋 초기화 + } } };📝 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.const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); const trimmedValue = inputValue.trim(); if (trimmedValue && !tags.includes(trimmedValue)) { setTags((prevTags) => [...prevTags, trimmedValue]); setInputValue(''); // 태그 추가 후 인풋 초기화 } } };
38-52: 🛠️ Refactor suggestion
태그 표시 UI 개선 및 접근성 고려
현재 구현은 다음과 같은 문제점이 있습니다:
- 태그가 absolute 위치로 지정되어 있어 여러 태그가 추가될 경우 겹칠 수 있습니다.
- 태그 삭제 버튼("x")이 접근성을 고려하지 않았습니다.
<div className="relative flex flex-wrap gap-2 mt-2"> {tags.map((tag, index) => ( <div key={index} - className="flex items-center bg-gray-200 rounded-lg px-2 py-1 absolute left-3 top-1" + className="flex items-center bg-gray-200 rounded-lg px-2 py-1 m-1" > <span className="text-body-5 text-gray-700">{tag}</span> - <span - className="ml-2 cursor-pointer text-red-500" + <button + type="button" + aria-label={`${tag} 태그 삭제`} + className="ml-2 cursor-pointer text-red-500" onClick={() => handleRemoveTag(tag)} > x - </span> + </button> </div> ))} </div>📝 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.<div className="relative flex flex-wrap gap-2 mt-2"> {tags.map((tag, index) => ( <div key={index} className="flex items-center bg-gray-200 rounded-lg px-2 py-1 m-1" > <span className="text-body-5 text-gray-700">{tag}</span> <button type="button" aria-label={`${tag} 태그 삭제`} className="ml-2 cursor-pointer text-red-500" onClick={() => handleRemoveTag(tag)} > x </button> </div> ))} </div>
src/features/card-detail/ui/underlineTabs.tsx (1)
29-38: 🛠️ Refactor suggestion
접근성 개선 필요
탭 구현에 대한 키보드 탐색 및 ARIA 속성이 누락되어 있습니다. 접근성을 개선하기 위해 적절한 ARIA 역할과 속성을 추가하세요.
+ <div + role="tablist" + className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)} + style={{ + scrollbarWidth: 'none', + msOverflowStyle: 'none', + }} + > + {tabs.map((tab) => ( + <button + key={tab.id} + role="tab" + aria-selected={activeTab === tab.id} + aria-controls={`panel-${tab.id}`} + id={`tab-${tab.id}`} onClick={() => onChange(tab.id)} className={cn( `mr-2 flex-shrink-0 transition-all duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`, activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600', )} > <Typography variant="body-3">{tab.label}</Typography> </button> ))} </div>📝 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.<div role="tablist" className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)} style={{ scrollbarWidth: 'none', msOverflowStyle: 'none', }} > {tabs.map((tab) => ( <button key={tab.id} role="tab" aria-selected={activeTab === tab.id} aria-controls={`panel-${tab.id}`} id={`tab-${tab.id}`} onClick={() => onChange(tab.id)} className={cn( `mr-2 flex-shrink-0 transition-all duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`, activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600', )} > <Typography variant="body-3">{tab.label}</Typography> </button> ))} </div>
src/features/home/hooks/prefetches/useCardPrefetch.ts (2)
19-24: 🛠️ Refactor suggestion
타입 안전성 개선 필요
params 및 반환 데이터에 'any' 타입을 사용하고 있습니다. 타입 안전성을 높이기 위해 명확한 타입 정의가 필요합니다.
- export const getCardPrefetch = (params: any, queryClient: QueryClient, cookie: ReadonlyRequestCookies) => { + interface CardQueryParams { + id?: string; + // 필요한 다른 파라미터들 추가 + } + + interface CardType { + // 카드 데이터 타입 정의 + id: string; + name: string; + // 기타 필요한 필드들 + } + + export const getCardPrefetch = (params: CardQueryParams, queryClient: QueryClient, cookie: ReadonlyRequestCookies) => { return queryClient.prefetchQuery({ queryKey: [CARD_QUERY_KEY, params], queryFn: () => _getCardPrefetch(params, cookie), }); };📝 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.interface CardQueryParams { id?: string; // 필요한 다른 파라미터들 추가 } interface CardType { // 카드 데이터 타입 정의 id: string; name: string; // 기타 필요한 필드들 } export const getCardPrefetch = ( params: CardQueryParams, queryClient: QueryClient, cookie: ReadonlyRequestCookies ) => { return queryClient.prefetchQuery({ queryKey: [CARD_QUERY_KEY, params], queryFn: () => _getCardPrefetch(params, cookie), }); };
8-17:
⚠️ Potential issueAPI 엔드포인트 실제 경로로 수정 필요
'/somewhere/server-side'는 실제 API 엔드포인트가 아닌 임시 플레이스홀더로 보입니다. 실제 사용할 API 엔드포인트로 수정이 필요합니다.
const _getCardPrefetch = async (params: any, cookie: ReadonlyRequestCookies) => { - const data = await server.get<any>('/somewhere/server-side', { + const data = await server.get<CardType>('/api/cards', { params, headers: { Cookie: cookie?.toString(), }, }); return data; };📝 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.const _getCardPrefetch = async (params: any, cookie: ReadonlyRequestCookies) => { const data = await server.get<CardType>('/api/cards', { params, headers: { Cookie: cookie?.toString(), }, }); return data; };
src/features/multi-step-form/ui/careerForm/index.tsx (1)
51-66: 🛠️ Refactor suggestion
스텝별 검증 로직 구현
각 스텝에 필요한 필드만 검증하는 로직이 잘 구현되었습니다. 다만
!fieldsToValidate
체크는 검증할 필드가 없는 경우(예: 스텝 2~4) 조기 반환하게 되어 이후 단계로 진행되지 않을 수 있습니다.다음과 같이 빈 배열인 경우에도 다음 단계로 진행되도록 수정하는 것이 좋겠습니다:
const handleNextStep = async () => { const fieldsToValidate = stepValidationFields[currentStep]; - if (!fieldsToValidate) return; + if (!fieldsToValidate) { + if (currentStep < TOTAL_STEPS) { + onNextStep(); + } else { + // 마지막 단계인 경우 form 제출 + handleSubmit(onSubmit)(); + } + return; + } const valid = await trigger(fieldsToValidate); if (valid) { if (currentStep < TOTAL_STEPS) { onNextStep(); } else { // 마지막 단계인 경우 form 제출 handleSubmit(onSubmit)(); } } };📝 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.// 각 스텝에 해당하는 필드만 trigger로 검증 후 다음 단계로 이동 const handleNextStep = async () => { const fieldsToValidate = stepValidationFields[currentStep]; if (!fieldsToValidate) { if (currentStep < TOTAL_STEPS) { onNextStep(); } else { // 마지막 단계인 경우 form 제출 handleSubmit(onSubmit)(); } return; } const valid = await trigger(fieldsToValidate); if (valid) { if (currentStep < TOTAL_STEPS) { onNextStep(); } else { // 마지막 단계인 경우 form 제출 handleSubmit(onSubmit)(); } } };
src/features/home/components/BusinessCard/Card.tsx (1)
90-111:
⚠️ Potential issueCardTags 컴포넌트 구현
태그 표시 로직에 문제가 있습니다. 코드의 주석과 실제 구현이 일치하지 않습니다. 4개 이상의 태그가 있을 때 처음 3개를 표시하고 나머지 갯수를 "+n" 형태로 보여주려는 의도같은데, "4개 이상"과 "3개 이상"의 불일치가 있습니다.
다음과 같이 수정해주세요:
let convertedTags = tags; - if (tags.length > 4) { + // 최대 3개의 태그를 표시하고, 나머지는 "+n" 형태로 표시 + if (tags.length > 3) { convertedTags = [...tags.slice(0, 3), `+ ${tags.length - 3}`]; }📝 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.type CardTagsProps = { tags: string[]; tagType: JopType; }; export const CardTags = ({ tags, tagType, ...rest }: CardTagsProps & HTMLAttributes<HTMLDivElement>) => { let convertedTags = tags; // 최대 3개의 태그를 표시하고, 나머지는 "+n" 형태로 표시 if (tags.length > 3) { convertedTags = [...tags.slice(0, 3), `+ ${tags.length - 3}`]; } return ( <div className="mt-[44px] flex flex-wrap items-center gap-1" {...rest}> {convertedTags.map((tag, index) => ( <CardTag key={`${tag}-${index}`} tagType={tagType}> {tag} </CardTag> ))} </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.
고생 하셨습니다.
const JOB_CONFIG: Record<JobType, JobConfig> = { | ||
DEVELOPER: { | ||
backgroundImage: '/images/card-detail/card-detail-develop.png', | ||
iconPath: '/icons/developer-icon-white.svg', | ||
iconAlt: '개발자 아이콘', | ||
}, | ||
DESIGNER: { | ||
backgroundImage: '/images/card-detail/card-detail-design.png', | ||
iconPath: '/icons/designer-icon-white.svg', | ||
iconAlt: '디자이너 아이콘', | ||
}, | ||
}; |
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.
@JaeIn1 config로 분리해서 관리한 부분이 너무 좋네요
const sectionRefs = { | ||
domains: useRef<HTMLDivElement>(null), | ||
sns: useRef<HTMLDivElement>(null), | ||
news: useRef<HTMLDivElement>(null), | ||
hobby: useRef<HTMLDivElement>(null), | ||
posts: useRef<HTMLDivElement>(null), | ||
projects: useRef<HTMLDivElement>(null), | ||
}; |
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.
ref
를 객체로 관리한 부분이 인상적이네요. 좋습니다.
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: 0
🧹 Nitpick comments (6)
src/features/card-detail/hooks/useTabsActive.ts (2)
1-2
: 파일명 주석 불일치
주석에서 "useTabSync.ts"라는 이름이 사용되고 있지만, 실제 파일명은 "useTabsActive.ts"입니다. 필요하다면 주석을 파일명과 일치시켜 가독성을 높이는 방안을 고려해 주세요.
17-35
: 기본 탭 처리 로직 검토 필요
모든 섹션이 보이지 않는 경우(모두 false)가 발생한다면, 활성화할 탭이 없어질 수 있습니다. 사용성 측면에서 기본 탭을 설정하거나 예외 처리를 추가하는 것이 좋겠습니다.src/shared/ui/appbar.tsx (1)
69-75
: 조건부 클래스 간소화 제안
page === 'detail' && hasBackground
,page === 'create'
모두 같은 배경 클래스를 적용하므로 다음과 같이 조건식을 간단히 표현하면 코드를 더 읽기 쉽게 만들 수 있습니다.- className={`z-100 sticky top-0 flex h-16 w-full max-w-[600px] items-center justify-between px-4 py-5 pb-5 pr-4 ${ - page === 'detail' && hasBackground ? 'bg-gray-black' : page === 'create' ? 'bg-gray-black' : '' - }`} + className={`z-100 sticky top-0 flex h-16 w-full max-w-[600px] items-center justify-between px-4 py-5 pb-5 pr-4 ${ + (page === 'detail' && hasBackground) || page === 'create' ? 'bg-gray-black' : '' + }`}src/features/card-detail/ui/cardTabs.tsx (3)
37-42
: Intersection Observer 임계값 확인
threshold: 1
을 사용하면 해당 섹션이 화면에 완전히 들어왔을 때만inView
가 true가 됩니다. 탭 전환이 다소 늦어진다고 느낄 수 있으므로, 사용자 경험에 맞는 적절한 임계값(예: 0.5 등)을 고려해 보세요.
80-96
: 하드코딩된 스크롤 마진 주의
헤더 높이에 대응하기 위해tabsScrollMargin
를 100px로 고정했는데, 추후 헤더 높이가 바뀌면 오프셋이 어긋날 수 있습니다. 실제 헤더 높이를 동적으로 계산하거나 상수 한 곳에서만 관리하도록 개선하는 방안을 검토해 주세요.
100-179
: 반복되는 여백 스타일 관리
px-[20px]
등 중복되는 여백 스타일이 여러 곳에서 반복됩니다. TailwindCSS의 커스텀 클래스나 상수화로 관리하여 유지보수성을 높일 수 있습니다.
📜 Review details
Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/app/(detail)/card-detail/[id]/page.tsx
(1 hunks)src/features/card-detail/hooks/useTabsActive.ts
(1 hunks)src/features/card-detail/ui/cardTabs.tsx
(1 hunks)src/shared/ui/appbar.tsx
(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/app/(detail)/card-detail/[id]/page.tsx
🔇 Additional comments (2)
src/shared/ui/appbar.tsx (1)
10-11
: hasBackground 속성 도입
hasBackground
를 통한 배경 처리 옵션이 명확하게 분리되어 UI 요구사항에 잘 대응합니다. 추가 수정 사항은 보이지 않습니다.src/features/card-detail/ui/cardTabs.tsx (1)
45-55
: 탭 동기화 로직 구현
useTabsActive
를 활용하여 현재 보이는 섹션에 따라 탭 상태를 업데이트하는 로직이 명확하게 구성되었습니다. 별다른 문제 없이 잘 동작할 것으로 보입니다.
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: 0
🧹 Nitpick comments (3)
src/features/card-detail/ui/cardTabs.tsx (2)
67-78
: ref 결합 로직의 개선 가능성이 있습니다.현재 if-else 체인을 사용하여 섹션 ref를 설정하고 있지만, 이 부분은 객체 매핑을 통해 더 간결하게 작성할 수 있습니다.
- // 각 섹션에 맞는 ref 설정 - if (section === 'domains') domainsRef(el); - else if (section === 'sns') snsRef(el); - else if (section === 'news') newsRef(el); - else if (section === 'hobby') hobbyRef(el); - else if (section === 'posts') postsRef(el); - else if (section === 'projects') projectsRef(el); + // 섹션에 맞는 ref 매핑 + const refMapping = { + domains: domainsRef, + sns: snsRef, + news: newsRef, + hobby: hobbyRef, + posts: postsRef, + projects: projectsRef, + }; + if (refMapping[section]) refMapping[section](el);
122-187
: 비슷한 구조의 섹션 컴포넌트를 재사용 가능한 컴포넌트로 추출할 수 있습니다.각 섹션이 매우 유사한 구조와 스타일을 가지고 있으므로, 이를 재사용 가능한 컴포넌트로 추출하면 코드의 중복을 줄이고 유지보수성을 높일 수 있습니다.
예를 들어, 다음과 같은 컴포넌트를 만들 수 있습니다:
type SectionProps = { id: TabId; title: string; children: React.ReactNode; combineRefs: (section: TabId) => (el: HTMLDivElement) => void; isLastSection?: boolean; }; function CardSection({ id, title, children, combineRefs, isLastSection = false }: SectionProps) { return ( <div ref={combineRefs(id)} id={id} className={`${spacingStyles({ paddingY: 'xl' })} ${ isLastSection ? 'pb-[77px]' : 'border-b-[4px] border-gray-800' } px-[20px]`} > <Typography variant="body-1">{title}</Typography> {children} </div> ); }이 컴포넌트를 사용하면 각 섹션 코드를 다음과 같이 간소화할 수 있습니다:
{data.data.interestDomain && ( <CardSection id="domains" title="관심 도메인" combineRefs={combineRefs} > <DomainList data={data.data.interestDomain} /> </CardSection> )}src/features/card-detail/types/cardDetail.ts (1)
15-29
: ContentItemDto와 ProjectDto 타입이 중복됩니다.ContentItemDto와 ProjectDto가 동일한 구조를 가지고 있습니다. 이 둘을 하나의 공통 타입으로 통합하여 코드 중복을 줄이고 일관성을 유지하는 것이 좋을 것 같습니다.
다음과 같이 수정할 수 있습니다:
// 콘텐츠 아이템 공통 타입 (블로그 포스트, 프로젝트 등) + export type ItemDto = { + title: string; + link: string; + imageUrl: string; + description: string; + }; - // 콘텐츠 아이템 (블로그 포스트 등) 타입 - export type ContentItemDto = { - title: string; - link: string; - imageUrl: string; - description: string; - }; - // 프로젝트 타입 - export type ProjectDto = { - title: string; - link: string; - imageUrl: string; - description: string; - }; + // 콘텐츠 아이템 (블로그 포스트 등) 타입 + export type ContentItemDto = ItemDto; + + // 프로젝트 타입 + export type ProjectDto = ItemDto;이렇게 하면 나중에 특정 타입에 필드를 추가해야 할 경우 확장이 용이합니다:
// 예: 프로젝트에만 필요한 추가 필드가 있을 경우 export type ProjectDto = ItemDto & { teamSize?: number; duration?: string; };
📜 Review details
Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/features/card-detail/types/cardDetail.ts
(1 hunks)src/features/card-detail/ui/cardTabs.tsx
(1 hunks)
🔇 Additional comments (8)
src/features/card-detail/ui/cardTabs.tsx (5)
4-6
: 필요한 모든 hook이 적절히 임포트되었습니다.React 및 react-intersection-observer의 hook을 적절히 가져오고 있어 좋습니다.
29-35
: 스크롤 감지 로직이 잘 구현되어 있습니다.스크롤 위치를 감지하여 Appbar의 표시 여부를 결정하는 로직이 깔끔하게 구현되어 있습니다.
threshold와 rootMargin 값의 주석 처리로 의도가 명확히 설명되어 있어 좋습니다.
57-65
: ref 객체를 효율적으로 관리하고 있습니다.여러 섹션에 대한 ref를 객체로 관리하여 코드의 가독성과 유지보수성을 높였습니다.
각 ref에 대한 타입 지정도 명확하게 되어있어 좋습니다.
111-111
: Appbar 컴포넌트에 hasBackground 속성이 추가되었습니다.PR 목표에 언급된 대로 명함 상세 페이지 요구사항을 충족하기 위해 Appbar 컴포넌트에 hasBackground 속성이 추가되었습니다.
이 변경으로 배경 스타일이 적절히 적용될 수 있습니다.
80-97
:❓ Verification inconclusive
스크롤 마진 값에 대한 더 유연한 접근이 필요합니다.
현재 하드코딩된 '100px' 값은 화면 크기에 따라 문제가 될 수 있습니다.
주석에도 언급되어 있듯이 임시 조정값이 포함되어 있습니다.- const tabsScrollMargin = '100px'; + // 헤더 높이와 추가 여백을 계산하는 방식으로 개선 + const headerHeight = 64; // 헤더 실제 높이 + const extraPadding = 36; // 추가 여백 + const tabsScrollMargin = `${headerHeight + extraPadding}px`;혹은 CSS 변수나 Tailwind 테마를 활용하여 일관된 스크롤 마진을 관리하는 것이 좋을 것 같습니다.
스크롤 마진 값 개선 요청
현재 스크롤 마진이 하드코딩된 "100px"로 설정되어 있어, 다양한 화면 크기에서 의도한 동작을 보장하기 어려울 수 있습니다. 제안드린 대로 헤더 높이(64px)와 추가 여백(36px)을 별도로 계산하여 적용하는 방식으로 수정하면, 코드의 가독성과 유지보수성이 향상될 것입니다.
- 아래와 같이 수정한 후, 모든 화면 크기에서 스크롤 동작이 정상 동작하는지 추가 검증 부탁드립니다.
- const tabsScrollMargin = '100px'; + // 헤더 높이와 추가 여백을 고려한 스크롤 마진 계산 + const headerHeight = 64; // 헤더 실제 높이 + const extraPadding = 36; // 추가 여백 + const tabsScrollMargin = `${headerHeight + extraPadding}px`;
- 또는 CSS 변수나 Tailwind 테마를 활용하여 스크롤 마진 값을 중앙집중적으로 관리하는 방법도 고려해 주세요.
src/features/card-detail/types/cardDetail.ts (3)
1-7
: API 응답 타입이 적절하게 정의되어 있습니다.API 응답에 대한 제네릭 타입이 잘 구성되어 있어 다양한 데이터 타입에 재사용할 수 있습니다.
47-48
: 응답 타입 조합이 적절히 되어있습니다.ApiResponse 제네릭 타입을 활용하여 CardDetailDto 타입을 정의한 것이 좋습니다. 이로써 API 응답 구조의 일관성을 유지할 수 있습니다.
31-45
:❓ Verification inconclusive
CardDetailResponse 타입의 구조가 잘 설계되어 있습니다.
명함 상세 정보에 필요한 모든 필드가 잘 정의되어 있고, 선택적 필드에는 물음표와 undefined를 함께 사용하여 명확하게 표시했습니다.
그러나 hobby와 news가 문자열로 정의된 반면, 다른 필드들은 배열로 정의되어 있어 일관성이 부족합니다. 데이터 구조를 더 일관되게 만드는 것을 고려해보세요.
🌐 Web query:
프론트엔드 애플리케이션에서 데이터 구조 일관성의 중요성
💡 Result:
프론트엔드 애플리케이션에서 데이터 구조 일관성은 시스템의 유지보수성, 확장성, 사용자 경험에 직접적인 영향을 미치는 핵심 요소입니다. 특히 복잡한 상태 관리와 다양한 데이터 소스의 통합이 요구되는 현대 웹 애플리케이션에서 이는 다음과 같은 측면에서 중요합니다.
시스템 유지보수성 향상
- 의존성 관리: 일관된 데이터 구조는 컴포넌트 간 의존성을 최소화하며, 모듈 간 결합도를 낮춰 변경 사항이 시스템 전체에 미치는 영향을 제한합니다[9][10]
- 코드 예측성 강화: 통일된 데이터 포맷(예: JSON Schema)을 사용하면 API 응답 처리 로직을 표준화할 수 있어 신규 개발자도 빠르게 코드베이스 이해 가능[11][2]
- 디버깅 효율화: 데이터 변환 계층(repository layer)에서 일관성 유지 시 오류 발생 지점을 신속하게 특정 가능[3][6]
성능 최적화
- 불필요한 재렌더링 방지: React 등의 프레임워크에서 상태 객체의 깊은 비교 대신 참조 비교만으로 변경 감지 가능[10]
- 캐싱 메커니즘 활용: 정규화된 데이터 구조를 적용하면 Apollo Client나 React Query 같은 라이브러리의 캐시 관리 기능을 최대화[5][8]
// 정규화된 데이터 구조 예시 const normalizedData = { users: { '1': { id: 1, name: 'John', posts: [101] }, '2': { id: 2, name: 'Jane', posts: [102] } }, posts: { '101': { id: 101, content: '...' }, '102': { id: 102, content: '...' } } }협업 효율성 증대
- 팀 간 커뮤니케이션 표준화: 프론트엔드-백엔드 팀이 공유하는 Swagger/OpenAPI 스펙과의 구조 일치성 유지[7][8]
- 컴포넌트 재사용성 향상: UI 컴포넌트가 기대하는 데이터 형식에 대한 명확한 계약 존재 시 독립적 개발 및 테스트 가능[2][11]
에러 방지 메커니즘
- 런타임 유효성 검증: Zod나 Yup을 이용한 데이터 구조 검증 계층 구현 시 잘못된 데이터 입력 차단[3]
import { z } from 'zod'; const UserSchema = z.object({ id: z.number(), name: z.string().min(2), email: z.string().email() });확장성 보장
- 다층 아키텍처 지원: Clean Architecture에서 도메인 계층의 순수 데이터 모델 유지는 기술 스택 변경 시 최소 영향도 보장[3][11]
- 마이크로 프론트엔드 호환: 독립적 배포가 가능한 서비스 간 데이터 교환 표준화[10][8]
사용자 경험 개선
- 데이터 동기화 보장: Optimistic UI 구현 시 일관된 구조가 로컬 상태와 서버 상태의 충돌 해결 용이[5]
- 로딩 상태 관리: Skeleton UI 표시를 위한 데이터 형식 사전 정의 가능[6][7]
프론트엔드에서 데이터 구조 일관성을 달성하기 위한 실제 구현 전략으로는 TypeScript 인터페이스 표준화[10], API 응답 변환 계층 도입[11], 상태 관리 라이브러리 선택 시 직렬화 가능성 고려[9] 등이 있습니다. 이는 단순히 기술적 완성도를 넘어 비즈니스 요구사항 변화에 신속히 대응할 수 있는 애자일한 시스템 구축의 토대가 됩니다[4][8].
Citations:
- 1: https://brunch.co.kr/@matthew-chang/59
- 2: https://velog.io/@skyoffly/%EA%B0%9C%EB%B0%9C-%EC%A7%80%EC%8B%9D-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98
- 3: https://hackernoon.com/lang/ko/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98%EC%97%90%EC%84%9C-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EB%A5%BC-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EB%8A%94-%EA%B2%83%EC%9D%B4-%ED%95%A9%EB%A6%AC%EC%A0%81%EC%9D%B8%EA%B0%80%EC%9A%94%3F
- 4: https://yozm.wishket.com/magazine/detail/1513/
- 5: https://brunch.co.kr/@yj5wqu/36
- 6: https://yozm.wishket.com/magazine/detail/555/
- 7: https://brunch.co.kr/@@hhpB/15
- 8: https://aws.amazon.com/ko/compare/the-difference-between-frontend-and-backend/
- 9: https://velog.io/@clydehan/%EA%B0%9C%EB%B0%9C%EC%97%90%EC%84%9C-%EC%9D%98%EC%A1%B4%EC%84%B1%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1-%EC%99%9C-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%98%EA%B3%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B4%80%EB%A6%AC%ED%95%A0%EA%B9%8C
- 10: https://velog.io/@kennys/%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%97%90%EC%84%9C-%EC%A2%8B%EC%9D%80-%EA%B5%AC%EC%A1%B0%EB%9E%80
- 11: https://imagineu.tistory.com/82
- 12: https://velog.io/@m_jae/2.-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4%EC%99%80-%EC%82%AC%EC%9A%A9%EC%9E%90-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B4%80%EB%A6%AC
- 13: https://careerly.co.kr/qnas/5729
- 14: https://velog.io/@jjunyjjuny/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EA%B0%80%EA%B3%B5%ED%95%98%EB%9D%BC
- 15: https://brunch.co.kr/@b8f8683a622d44b/40
- 16: https://prgms.tistory.com/225
- 17: https://www.miraeweb.com/single-post/2016/11/17/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%9B%B9%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90-%EB%B9%84%EA%B5%90%EB%B6%84%EC%84%9D-mvc%EC%99%80-mvvm
- 18: https://post.naver.com/viewer/postView.naver?volumeNo=36773550&memberNo=62742422
- 19: https://velog.io/@teo/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%97%90%EA%B2%8C-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B3%B5%EB%B6%80%EA%B0%80-%EB%AF%B8%EC%B9%98%EB%8A%94-%EC%98%81%ED%96%A5
- 20: https://sennieworld.tistory.com/67
검증 결과, 타입 구조는 전반적으로 명확하게 정의되어 있습니다만,
news
와hobby
가 문자열로 지정된 점은 다른 선택적 필드(배열 타입)와의 일관성 측면에서 재검토가 필요해 보입니다.
- 파일:
src/features/card-detail/types/cardDetail.ts
(Lines 31–45)- 제안:
- 만약 해당 필드들이 여러 값을 다뤄야 한다면, 타입을
string[]
등 배열 형태로 변경하여 다른 필드와의 데이터 구조 일관성을 맞추는 방안을 고려해 주세요.- 단, 해당 필드들이 실제로 단일 값을 목적으로 한다면 현재 구현도 문제가 없으므로, 데이터 처리 로직과 UI 컴포넌트 간의 계약이 명확하게 일치하는지 확인해 주시기 바랍니다.
프론트엔드 애플리케이션에서는 데이터 구조 일관성이 유지보수성과 효율적인 디버깅에 큰 도움이 되므로, 이 부분에 대해 검토 후 필요 시 수정해주시길 권장합니다.
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.
수고 많으셨어요!!! 👍
<div className="sticky top-0 z-10 w-full"> | ||
{/* Appbar에 트랜지션 효과 추가 */} | ||
<div | ||
className={`transition-all duration-300 ease-in-out ${showAppbar ? 'max-h-16 opacity-100' : 'max-h-0 overflow-hidden opacity-0'}`} |
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.
조건문이 있는 className은 cn 함수를 사용해서 구현하면 좋을 것 같아요!
className={cn('transition-all duration-300 ease-in-out',
showAppbar ? 'max-h-16 opacity-100' : 'max-h-0 overflow-hidden opacity-0')}
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.
오 그러네요 반영해서 수정하겠습니다!!
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: 0
🧹 Nitpick comments (3)
src/features/card-detail/ui/cardDetailHeader.tsx (1)
14-17
: 기본 직군(DEVELOPER) 처리에 대한 유연성 제고현재
userJob
을 계산할 때'DEVELOPER'
문자열이 기본값으로 설정되어 있습니다. 예기치 않은 직군 타입이 들어오는 경우를 대비하여, 별도 에러 처리를 하거나 다른 직군 타입에 대한 기본 동작을 정의해두면 더욱 안전한 코드가 될 수 있습니다.src/features/card-detail/ui/cardTabs.tsx (2)
30-37
: 교차점(threshold) 동작 범위 재검토
useScrollPosition
사용 시threshold: 0
설정으로, 요소의 상단이 뷰포트에 일부만 보여도 감지됩니다. 의도대로 정확히 상단 교차 지점을 포착하고 싶은 경우, 임계값을 더 세밀하게 조정하거나 rootMargin을 상황에 맞추어 수정하는 방안을 고려하세요.
81-98
: 탭 전환 로직에 대한 스크롤 위치 계산 보완
handleTabChange
내에서scrollMarginTop
을 하드코딩해100px
으로 지정했는데, 헤더의 높이가 달라질 경우 유지보수 측면에서 버그 발생 가능성이 있습니다. 헤더 노드의 실제 높이를 구해 동적으로 margin 값을 설정하면 더욱 유연하게 탭 전환을 처리할 수 있습니다.const handleTabChange = (tabId: TabId) => { setActiveTab(tabId); - const tabsScrollMargin = '100px'; + const headerElement = document.querySelector('.card-detail-header'); + const headerHeight = headerElement?.clientHeight ?? 64; + const extraMargin = 36; // 여유 영역 + const tabsScrollMargin = `${headerHeight + extraMargin}px`; if (sectionRefs[tabId]?.current) { sectionRefs[tabId].current.style.scrollMarginTop = tabsScrollMargin; sectionRefs[tabId].current.scrollIntoView({ behavior: 'smooth', block: 'start', }); } };
📜 Review details
Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/app/(detail)/card-detail/[cardId]/page.tsx
(1 hunks)src/features/card-detail/hooks/query/useCardDetailQuery.ts
(1 hunks)src/features/card-detail/ui/cardDetailHeader.tsx
(1 hunks)src/features/card-detail/ui/cardTabs.tsx
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/features/card-detail/hooks/query/useCardDetailQuery.ts
🔇 Additional comments (2)
src/app/(detail)/card-detail/[cardId]/page.tsx (1)
4-13
: Page 컴포넌트의 에러 처리 및 상태 관리 확인 필요페이지 전체가 CardDetailHeader 및 CardTabs 컴포넌트에 의존하여 데이터를 표시하고 있습니다. 만약
cardId
가 유효하지 않거나, 쿼리로부터 데이터를 불러오는 동안 에러가 발생할 경우를 대비해 에러 처리나 로딩 상태를 추가로 고려해보면 좋겠습니다.src/features/card-detail/ui/cardDetailHeader.tsx (1)
44-61
:data?.data
사용 시 null 체크 로직 보강 제안사용자 정보(
nickname
,organization
,summary
)를 렌더링할 때data?.data
를 통해 안전하게 접근 중이지만, 실제 API 호출 결과가 null 또는 빈 객체일 경우 UI가 정상적으로 표시되지 않을 수 있습니다. 로딩 상태나 에러 상태를 처리할 예외 로직을 추가로 고려해 보시면 좋겠습니다.
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.
@JaeIn1 몇가지 코멘트 남겼습니다. 정말 고생많으셨어요~~ 고생의 흔적이 보이네요.
// 각 섹션의 가시성에 따라 활성 탭 설정 | ||
if (viewState.domains) setActiveTab('domains'); | ||
else if (viewState.sns) setActiveTab('sns'); | ||
else if (viewState.news) setActiveTab('news'); | ||
else if (viewState.hobby) setActiveTab('hobby'); | ||
else if (viewState.posts) setActiveTab('posts'); | ||
else if (viewState.projects) setActiveTab('projects'); |
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.
@JaeIn1 지금도 충분히 가독성이 좋다고 생각하지만 다른 관점도 제시해주기 위해 레퍼런스 하나 놓고 갑니다~~
ts에서 조건문 같은경우 ts-pattern이라는 강력한 도구가 있습니다. 또한 룩업 테이블
라는 참조 연산할때 좋은 개념이 있는데 지금과는 조금 벗어날수도 있지만 한번 살펴 보시면 좋을듯합니다.
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.
@jangwonyoon 오 처음 알았네요. 참고해서 한 번 학습해보겠습니다!!
export type ApiResponse<T> = { | ||
status: string; | ||
message: string; | ||
timestamp: string; | ||
data: T; | ||
}; |
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.
@JaeIn1 제네릭으로 처리한 부분 좋습니다~
const sectionRefs = { | ||
domains: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>, | ||
sns: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>, | ||
news: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>, | ||
hobby: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>, | ||
posts: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>, | ||
projects: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>, | ||
}; | ||
|
||
// 기존 ref와 intersection observer ref를 결합하는 함수 | ||
const combineRefs = (section: TabId) => (el: HTMLDivElement) => { | ||
sectionRefs[section].current = el; | ||
|
||
// 각 섹션에 맞는 ref 설정 | ||
if (section === 'domains') domainsRef(el); | ||
else if (section === 'sns') snsRef(el); | ||
else if (section === 'news') newsRef(el); | ||
else if (section === 'hobby') hobbyRef(el); | ||
else if (section === 'posts') postsRef(el); | ||
else if (section === 'projects') projectsRef(el); | ||
}; |
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.
결과값을 참조하는 부분도 룩업 테이블을 통해 빠른 참조와 가독성있게 나타낼수 있어보입니다. 물론 지금도 충분합니다~
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.
@jangwonyoon 룩업 테이블 학습해보겠습니다!!
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.
wow 고생하셨네요.. 짱짱
탭 이동 시에 anchor tag + hash change event 사용해도 좋을 것 같아요.
요런 방법도 있구나 정도!
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.
@Jungjjeong 오 생각해보니까 그 방법도 있었네요 다시 한 번 봐보겠습니다!!
|
||
function Posts({ data }: PostsProps) { | ||
return ( | ||
<div className={`${spacingStyles({ marginTop: 'ms' })}`}> |
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.
마진이 들어가는곳에 명시적으로 spacingStyles
스타일 써주신 부분 좋습니다. 감사해요
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.
@jangwonyoon 👍👍
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.
👍
export interface TabItem { | ||
id: TabId; | ||
label: string; | ||
} |
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.
다른 컴포넌트에서 동일한 타입이 다시 선언된 것 같아요.
export의 의도가 다른 곳에서도 사용하는 것이라면, 재사용해도 될 것 같습니다.
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.
@Jungjjeong 확인했습니다.. 반영해서 다시 올리겠습니다!!
</div> | ||
</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.
불필요한 fragment가 들어갔어요!
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.
@Jungjjeong 엇 그러네요.. 수정하겠습니다!!
const sectionRefs = { | ||
domains: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>, | ||
sns: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>, | ||
news: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>, | ||
hobby: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>, | ||
posts: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>, | ||
projects: useRef<HTMLDivElement | null>(null) as React.MutableRefObject<HTMLDivElement | null>, | ||
}; | ||
|
||
// 기존 ref와 intersection observer ref를 결합하는 함수 | ||
const combineRefs = (section: TabId) => (el: HTMLDivElement) => { | ||
sectionRefs[section].current = el; | ||
|
||
// 각 섹션에 맞는 ref 설정 | ||
if (section === 'domains') domainsRef(el); | ||
else if (section === 'sns') snsRef(el); | ||
else if (section === 'news') newsRef(el); | ||
else if (section === 'hobby') hobbyRef(el); | ||
else if (section === 'posts') postsRef(el); | ||
else if (section === 'projects') projectsRef(el); | ||
}; |
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.
wow 고생하셨네요.. 짱짱
탭 이동 시에 anchor tag + hash change event 사용해도 좋을 것 같아요.
요런 방법도 있구나 정도!
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: 0
🧹 Nitpick comments (4)
src/features/card-detail/ui/underlineTabs.tsx (2)
9-9
:activeTab
타입을TabId
로 통일해주세요.
TabId
타입을 이미 import했음에도activeTab
이string
으로 선언되어 있어 타입이 불일치합니다.onChange
가TabId
를 사용하는 만큼activeTab
도TabId
타입으로 맞추면 오류를 줄이고 가독성이 향상됩니다.- activeTab: string; + activeTab: TabId;
14-37
: 접근성(ARIA 속성) 관련 개선을 고려해주세요.
각 탭에role="tab"
를 부여하고, 탭들을 감싼 컨테이너에role="tablist"
를 지정하면 화면 읽기 도구 등에서 탭 구조를 더욱 명확히 인식할 수 있습니다. 또한, 선택된 탭에는aria-selected="true"
를, 나머지 탭에는aria-selected="false"
와 같은 속성을 설정해 접근성을 높일 수 있습니다.<div + role="tablist" className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)} style={{ scrollbarWidth: 'none', msOverflowStyle: 'none', }} > {tabs.map((tab) => ( <button + role="tab" + aria-selected={activeTab === tab.id} key={tab.id} onClick={() => onChange(tab.id)} className={cn( `mr-2 flex-shrink-0 transition-all duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`, activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600', )} > <Typography variant="body-3">{tab.label}</Typography> </button> ))} </div>src/features/card-detail/ui/cardTabs.tsx (1)
90-90
:tabsScrollMargin
값에 대한 하드코딩을 최소화해주세요.
'100px'
은 하드코딩된 스타일 값이므로, 추후 레이아웃이나 헤더 높이가 변경되면 유지보수가 어려울 수 있습니다. 상수나 테마 파일에서 관리해 가독성과 확장성을 높이는 방법을 고려해 보세요.- const tabsScrollMargin = '100px'; + // 테마 상수 혹은 별도 변수로 재관리 + const TABS_SCROLL_MARGIN = '100px'; ... sectionRefs[tabId].current.style.scrollMarginTop = TABS_SCROLL_MARGIN;src/features/card-detail/ui/cardDetailHeader.tsx (1)
35-39
: Avatar 아이콘에 맞는alt
텍스트 사용을 권장드립니다.
현재 alt가"Settings"
로 되어 있는데, 이 아이콘은 사용자 프로필을 위한 이미지이므로"User Avatar"
등의 표현이 더욱 적합해 보입니다. 시각장애인 등을 고려한 적절한 대체 텍스트를 제공해주세요.- <Image src="/icons/avatarIcon.svg" alt="Settings" width="28" height="28" className="rounded-full" /> + <Image src="/icons/avatarIcon.svg" alt="User Avatar" width="28" height="28" className="rounded-full" />
📜 Review details
Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/features/card-detail/ui/cardDetailHeader.tsx
(1 hunks)src/features/card-detail/ui/cardTabs.tsx
(1 hunks)src/features/card-detail/ui/underlineTabs.tsx
(1 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
src/features/card-detail/ui/cardDetailHeader.tsx (1)
src/features/card-detail/hooks/query/useCardDetailQuery.ts (1) (1)
useCardDetailQuery
(17:24)
🔇 Additional comments (4)
src/features/card-detail/ui/cardTabs.tsx (2)
28-28
: 에러 처리 및 로딩 상태 처리가 필요합니다.
현재useCardDetailQuery
로 데이터를 불러올 때 로딩 및 에러 상황에 대한 처리가 보이지 않습니다. 데이터 요청이 실패하거나 늦어질 경우 UI가 깨질 수 있으니, 에러 상태 및 로딩 상태를 처리하는 로직을 추가하는 것을 권장드립니다.
38-43
:threshold: 1
설정 의도 확인 요청
현재 각useInView
훅이threshold: 1
로 되어 있어 요소가 뷰포트에 100% 보여야만inView
가true
로 바뀝니다. 섹션이 크게 길어질 경우 조금만 스크롤해도 탭이 활성화되어야 하는 상황에서는 동작이 달라질 수 있으니, 의도한 동작인지 다시 한번 확인해 주세요.src/features/card-detail/ui/cardDetailHeader.tsx (2)
14-14
: 로딩 상태와 에러 처리를 추가해주세요.
useCardDetailQuery
호출 시 에러 발생 또는 로딩 중 상태가 고려되지 않은 것으로 보입니다. 데이터가 도착하기 전이나 에러가 발생했을 때, 상단에 오류 안내나 로딩 스피너 등을 표시하는 로직을 추가하면 사용자 경험을 향상할 수 있습니다.
16-16
:job
값이 없는 경우 'DEVELOPER'로 설정되는 부분을 재확인해주세요.
백엔드에서job
이 빈 문자열 혹은null
로 내려올 경우 자동으로 'DEVELOPER'를 사용합니다. 의도된 기본값인지 다시 한번 검토 부탁드립니다. 실제로는 다른 직군인데 데이터가 비어 있을 때 잘못된 정보가 노출될 여지가 있습니다.
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 (3)
src/features/card-detail/ui/underlineTabs.tsx (3)
7-12
: 인터페이스 타입 정의가 잘 되어 있습니다.인터페이스 정의가 명확하게 되어 있으나, 한 가지 개선 사항이 있습니다.
activeTab
이string
타입으로 정의되어 있는데, 이를 더 타입 안전하게TabId
타입으로 지정하는 것이 좋겠습니다.interface UnderlineTabsProps { tabs: TabItem[]; - activeTab: string; + activeTab: TabId; onChange: (tabId: TabId) => void; className?: string; }
14-37
: 스크롤 가능한 탭 구현이 잘 되어 있습니다.컴포넌트 구현이 전반적으로 잘 되어 있지만, 몇 가지 개선할 점이 있습니다:
- 모든 탭 버튼에
mr-2
클래스가 적용되어 있는데, 마지막 탭에는 불필요한 여백이 생길 수 있습니다.- 스크롤바는 숨겨져 있지만, 사용자에게 스크롤 가능하다는 표시가 없습니다.
transition-all
은 모든 CSS 속성에 대해 트랜지션을 적용하므로 성능에 영향을 줄 수 있습니다.아래는 개선된 구현 예시입니다:
export const UnderlineTabs = ({ tabs, activeTab, onChange, className }: UnderlineTabsProps) => { return ( <div className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)} style={{ scrollbarWidth: 'none', msOverflowStyle: 'none', }} > {tabs.map((tab, index) => ( <button key={tab.id} onClick={() => onChange(tab.id)} className={cn( - `mr-2 flex-shrink-0 transition-all duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`, + `flex-shrink-0 transition-colors duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`, + index < tabs.length - 1 && 'mr-2', activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600', )} > <Typography variant="body-3">{tab.label}</Typography> </button> ))} </div> ); };
16-22
: 스크롤 기능 개선 필요현재 구현은 스크롤바를 숨기고 있지만, 사용자에게 가로 스크롤 가능하다는 시각적 힌트가 없습니다. 또한 웹 접근성 측면에서도 개선이 필요합니다.
<div className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)} style={{ scrollbarWidth: 'none', msOverflowStyle: 'none', }} + role="tablist" + aria-orientation="horizontal" >또한 스크롤 인디케이터나 그라데이션 효과를 추가하는 것을 고려해보세요.
📜 Review details
Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/features/card-detail/config/tabs-config.ts
(1 hunks)src/features/card-detail/ui/underlineTabs.tsx
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/features/card-detail/config/tabs-config.ts
🧰 Additional context used
🧬 Code Definitions (1)
src/features/card-detail/ui/underlineTabs.tsx (1)
src/features/card-detail/config/tabs-config.ts (2) (2)
TabItem
(2:5)TabId
(1:1)
{tabs.map((tab) => ( | ||
<button | ||
key={tab.id} | ||
onClick={() => onChange(tab.id)} | ||
className={cn( | ||
`mr-2 flex-shrink-0 transition-all duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`, | ||
activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600', | ||
)} | ||
> | ||
<Typography variant="body-3">{tab.label}</Typography> | ||
</button> | ||
))} |
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.
🛠️ Refactor suggestion
탭 버튼의 접근성 개선 필요
현재 탭 버튼에는 적절한 ARIA 속성이 누락되어 있습니다. 접근성을 개선하기 위해 WAI-ARIA 규칙을 따른 속성을 추가하는 것이 좋습니다.
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => onChange(tab.id)}
+ role="tab"
+ aria-selected={activeTab === tab.id}
+ aria-controls={`panel-${tab.id}`}
+ id={`tab-${tab.id}`}
className={cn(
`mr-2 flex-shrink-0 transition-all duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`,
activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600',
)}
>
<Typography variant="body-3">{tab.label}</Typography>
</button>
))}
📝 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.
{tabs.map((tab) => ( | |
<button | |
key={tab.id} | |
onClick={() => onChange(tab.id)} | |
className={cn( | |
`mr-2 flex-shrink-0 transition-all duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`, | |
activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600', | |
)} | |
> | |
<Typography variant="body-3">{tab.label}</Typography> | |
</button> | |
))} | |
{tabs.map((tab) => ( | |
<button | |
key={tab.id} | |
onClick={() => onChange(tab.id)} | |
role="tab" | |
aria-selected={activeTab === tab.id} | |
aria-controls={`panel-${tab.id}`} | |
id={`tab-${tab.id}`} | |
className={cn( | |
`mr-2 flex-shrink-0 transition-all duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`, | |
activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600', | |
)} | |
> | |
<Typography variant="body-3">{tab.label}</Typography> | |
</button> | |
))} |
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 (3)
src/features/card-detail/ui/underlineTabs.tsx (3)
7-12
: 타입 일관성 개선 필요
activeTab
프로퍼티가string
타입으로 정의되어 있지만, 실제로는TabId
타입과 비교하고 있습니다. 더 타입 안전한 코드를 위해activeTab
도TabId
타입으로 정의하는 것이 좋습니다.interface UnderlineTabsProps { tabs: TabItem[]; - activeTab: string; + activeTab: TabId; onChange: (tabId: TabId) => void; className?: string; }
18-21
: 인라인 스타일 대신 CSS 클래스 사용 권장인라인 스타일 대신 CSS 클래스를 사용하는 것이 코드 유지보수성과 일관성을 높이는 데 도움이 됩니다. 특히 스크롤바 숨김과 같은 스타일링은 재사용 가능한 클래스로 분리하는 것이 좋습니다.
<div - className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)} - style={{ - scrollbarWidth: 'none', - msOverflowStyle: 'none', - }} + className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black hide-scrollbar', className)} >그리고 CSS 파일에 다음과 같은 클래스를 추가하세요:
.hide-scrollbar { scrollbar-width: none; -ms-overflow-style: none; } .hide-scrollbar::-webkit-scrollbar { display: none; }
1-39
: 키보드 탐색 기능 추가 필요현재 구현에는 키보드 탐색 기능이 누락되어 있습니다. 접근성 표준에 따르면 사용자가 키보드로 탭 사이를 이동할 수 있어야 합니다(보통 방향키 사용).
키보드 탐색을 지원하기 위해 키보드 이벤트 핸들러를 추가하세요:
export const UnderlineTabs = ({ tabs, activeTab, onChange, className }: UnderlineTabsProps) => { + const handleKeyDown = (e: React.KeyboardEvent, tabId: TabId, index: number) => { + if (e.key === 'ArrowRight') { + e.preventDefault(); + const nextIndex = (index + 1) % tabs.length; + onChange(tabs[nextIndex].id); + document.getElementById(`tab-${tabs[nextIndex].id}`)?.focus(); + } else if (e.key === 'ArrowLeft') { + e.preventDefault(); + const prevIndex = (index - 1 + tabs.length) % tabs.length; + onChange(tabs[prevIndex].id); + document.getElementById(`tab-${tabs[prevIndex].id}`)?.focus(); + } + }; return ( <div role="tablist" aria-orientation="horizontal" className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)} style={{ scrollbarWidth: 'none', msOverflowStyle: 'none', }} > - {tabs.map((tab) => ( + {tabs.map((tab, index) => ( <button role="tab" key={tab.id} onClick={() => onChange(tab.id)} + onKeyDown={(e) => handleKeyDown(e, tab.id, index)} + tabIndex={activeTab === tab.id ? 0 : -1} className={cn( `mr-2 flex-shrink-0 transition-colors duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`, activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600', )} > <Typography variant="body-3">{tab.label}</Typography> </button> ))} </div> ); };
📜 Review details
Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/features/card-detail/ui/underlineTabs.tsx
(1 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
src/features/card-detail/ui/underlineTabs.tsx (1)
src/features/card-detail/config/tabs-config.ts (2) (2)
TabItem
(2:5)TabId
(1:1)
🔇 Additional comments (1)
src/features/card-detail/ui/underlineTabs.tsx (1)
23-35
: 탭 버튼의 접근성 개선 필요현재 탭 버튼에는 적절한 ARIA 속성이 일부 누락되어 있습니다. 접근성을 개선하기 위해 WAI-ARIA 규칙을 따른 속성을 추가하는 것이 좋습니다.
{tabs.map((tab) => ( <button role="tab" key={tab.id} onClick={() => onChange(tab.id)} + aria-selected={activeTab === tab.id} + aria-controls={`panel-${tab.id}`} + id={`tab-${tab.id}`} className={cn( `mr-2 flex-shrink-0 transition-colors duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`, activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600', )} > <Typography variant="body-3">{tab.label}</Typography> </button> ))}
<div | ||
className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)} | ||
style={{ | ||
scrollbarWidth: 'none', | ||
msOverflowStyle: 'none', | ||
}} | ||
> | ||
{tabs.map((tab) => ( | ||
<button | ||
role="tab" | ||
key={tab.id} | ||
onClick={() => onChange(tab.id)} | ||
className={cn( | ||
`mr-2 flex-shrink-0 transition-colors duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`, | ||
activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600', | ||
)} | ||
> | ||
<Typography variant="body-3">{tab.label}</Typography> | ||
</button> | ||
))} | ||
</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.
🛠️ Refactor suggestion
탭 컨테이너에 대한 접근성 마크업 추가 필요
탭 인터페이스의 접근성을 개선하기 위해 컨테이너 요소에 적절한 역할(role)과 ARIA 속성을 추가해야 합니다. 또한 탭 패널도 구현되어야 완전한 탭 인터페이스가 됩니다.
<div
+ role="tablist"
+ aria-orientation="horizontal"
className={cn('flex w-full overflow-x-auto whitespace-nowrap bg-black', className)}
style={{
scrollbarWidth: 'none',
msOverflowStyle: 'none',
}}
>
{tabs.map((tab) => (
<button
role="tab"
key={tab.id}
onClick={() => onChange(tab.id)}
className={cn(
`mr-2 flex-shrink-0 transition-colors duration-200 ${spacingStyles({ paddingX: 'sm', paddingY: 'ms' })}`,
activeTab === tab.id ? 'border-b-2 border-gray-white text-gray-white' : 'text-gray-600',
)}
>
<Typography variant="body-3">{tab.label}</Typography>
</button>
))}
</div>
추가로, 이 컴포넌트와 함께 사용될 탭 패널 컴포넌트도 필요합니다. 예를 들면:
// 탭 패널 컴포넌트 예시
{tabs.map((tab) => (
<div
key={tab.id}
role="tabpanel"
id={`panel-${tab.id}`}
aria-labelledby={`tab-${tab.id}`}
hidden={activeTab !== tab.id}
>
{/* 탭 내용 */}
</div>
))}
📌 개요
📋 변경사항
기능
화면
| 기능 | 스크린샷 |
2025-03-18.7.24.57.mov
전달사항
✅ 체크사항
Summary by CodeRabbit
Summary by CodeRabbit
새로운 기능
PostThumbnail
컴포넌트가 추가되어 썸네일을 클릭하면 링크를 새 탭에서 열 수 있습니다.UnderlineTabs
컴포넌트가 추가되어 탭 탐색 기능이 향상되었습니다.CardDetailHeader
,CardTabs
,SNS
컴포넌트가 추가되어 카드 세부정보를 보다 효과적으로 표시합니다.useScrollPosition
,useTabsActive
,useCardDetailQuery
훅이 추가되어 사용자 경험을 개선합니다.스타일 개선