Feat(client): tooltip component 구현#262
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. Walkthrough기존 Tooltip 컴포넌트를 TooltipCard로 이름 변경하고, 새로운 Balloon 컴포넌트를 추가하여 UI 컴포넌트 라이브러리를 확장했습니다. JobPins 페이지에서 Balloon 컴포넌트를 활용한 리치 UI로 업데이트했습니다. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/client/src/shared/components/tooltipCard/TooltipCard.tsx (1)
14-25:⚠️ Potential issue | 🟠 Major하드코딩된
id="info-card"— 다중 인스턴스 시 중복 ID 위험
id="info-card"가 정적으로 고정되어 있어, 같은 페이지에TooltipCard가 두 개 이상 렌더링될 경우 HTML 표준 위반(중복 ID)이 발생하고aria-describedby가 의도치 않은 요소를 참조하게 됩니다. 또한role="tooltip"이 없어 보조 기술이 해당 요소를 툴팁으로 인식하지 못합니다.React의
useId훅을 활용해 인스턴스별 고유 ID를 생성하고,role="tooltip"을 추가하는 것을 권장합니다.🔧 수정 제안
+import { useId } from 'react'; import { Icon } from '@pinback/design-system/icons'; import InfoCard from './InfoCard'; export default function TooltipCard() { + const tooltipId = useId(); return ( <div className="mt-[0.8rem] flex items-center"> <p className="body3-r text-font-gray-3"> 일부 콘텐츠는 제목·이미지가 불러와지지 않을 수 있어요. </p> <div className="relative inline-flex items-center"> <button type="button" - aria-describedby="info-card" + aria-describedby={tooltipId} className="peer rounded p-[0.4rem]" > <Icon name="ic_info" size={16} /> </button> <div - id="info-card" + id={tooltipId} + role="tooltip" className="pointer-events-none absolute left-0 top-[calc(100%+8px)] z-[100] opacity-0 transition-opacity duration-150 peer-hover:opacity-100 peer-focus-visible:opacity-100" > <InfoCard /> </div> </div> </div> ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/client/src/shared/components/tooltipCard/TooltipCard.tsx` around lines 14 - 25, TooltipCard currently uses a hardcoded id ("info-card") for the tooltip which causes duplicate IDs and broken aria-describedby links when multiple TooltipCard instances render; update the TooltipCard component to call React's useId (or generate a stable unique id) and use that id for both the button's aria-describedby and the tooltip container's id, and also add role="tooltip" to the tooltip container (the element rendering InfoCard) so assistive tech recognizes it; locate the id usage in TooltipCard and the tooltip div that renders <InfoCard /> and replace the static id with the generated unique id and add the role attribute.
🧹 Nitpick comments (1)
apps/client/src/shared/components/balloon/Balloon.tsx (1)
19-22:variantStyle객체를 컴포넌트 외부로 이동
variantStyle은 props에 의존하지 않는 순수 상수이므로 컴포넌트 함수 내부에 정의할 필요가 없습니다. 현재 구조에서는 매 렌더마다 객체가 재생성됩니다. 컴포넌트 외부로 이동하면 불필요한 재생성을 방지할 수 있습니다.♻️ 수정 제안
+const variantStyle: Record<BalloonVariant, string> = { + gray: 'bg-gray900 text-white', + main: 'bg-main500 text-white', +}; + export function Balloon({ variant = 'gray', side = 'bottom', onClose, children, }: BalloonProps) { - const variantStyle = { - gray: 'bg-gray900 text-white', - main: 'bg-main500 text-white', - }; - return (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/client/src/shared/components/balloon/Balloon.tsx` around lines 19 - 22, Move the variantStyle constant out of the Balloon component so it isn't re-created on every render: locate the variantStyle object (currently defined inside the Balloon component in Balloon.tsx) and hoist it to module scope above the Balloon function, keeping the same key names ('gray', 'main') and values; ensure the Balloon component continues to reference variantStyle unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/client/src/pages/jobPins/JobPins.tsx`:
- Line 13: The temporary alert handler on the Balloon component (onClose={() =>
alert('닫힘')}) must be removed or replaced: locate the Balloon usage in
JobPins.tsx and either remove the onClose prop or replace it with a non-blocking
stub that clearly marks intent (e.g., a TODO handler that calls a close function
or dispatches a close action) and add a TODO comment referencing Balloon.onClose
to implement real close logic later; ensure no alert() calls remain in
production code.
- Line 7: In the JobPins component (JobPins.tsx) remove the unnecessary JSX
whitespace token {' '} that appears immediately after the <div> element (an
artifact of editing) so the <div> children render without the extraneous blank
node; locate the stray {' '} near the top of the component and delete it leaving
the surrounding JSX intact.
- Around line 5-35: The JobPins page currently contains only hardcoded Balloon
demo markup; replace this with real data fetching and state management: add
state (e.g., const [pins, setPins] = useState<JobPin[]>() ) and a data loader
(useEffect or React Query/SWR) that calls your job pins API (e.g., fetchJobPins
or getSavedJobPins) to populate pins, render the list by mapping pins to
<Balloon ...> elements instead of the static examples, show loading/error UI
while fetching, implement onClose handlers that call an unpin API (e.g.,
removeJobPin or unpinJob) and update state optimistically (update setPins to
remove the pin on success or rollback on failure), and ensure prop typings for
the JobPins component and children so Balloon receives the correct title,
subtitle, icon, and id values from each pin.
In `@apps/client/src/shared/components/balloon/Balloon.tsx`:
- Around line 34-38: 조건부로 렌더되는 닫기 버튼 (onClose 핸들러와 함께 렌더되는 <button> 바로 그 요소)에
accessibility와 form 동작 문제를 고치기 위해 버튼에 명시적 type="button" 속성을 추가하고 스크린 리더용
aria-label을 추가하세요 (예: aria-label="닫기" 또는 컴포넌트에 전달되는 레이블 prop 사용). 변경 대상 식별자:
onClose 조건부 버튼 렌더링과 Icon name="ic_close" 사용 부분(Balloon.tsx의 해당 버튼)을 찾아 해당 요소에
type="button"과 적절한 aria-label을 설정하면 됩니다.
- Around line 1-11: The file uses the React namespace type in BalloonProps
(children: React.ReactNode) but does not import React; add an explicit import
for React (e.g., import React from 'react') at the top of Balloon.tsx so the
React symbol is available for the BalloonProps type and other references such as
BalloonVariant and the Balloon component that use React types.
---
Outside diff comments:
In `@apps/client/src/shared/components/tooltipCard/TooltipCard.tsx`:
- Around line 14-25: TooltipCard currently uses a hardcoded id ("info-card") for
the tooltip which causes duplicate IDs and broken aria-describedby links when
multiple TooltipCard instances render; update the TooltipCard component to call
React's useId (or generate a stable unique id) and use that id for both the
button's aria-describedby and the tooltip container's id, and also add
role="tooltip" to the tooltip container (the element rendering InfoCard) so
assistive tech recognizes it; locate the id usage in TooltipCard and the tooltip
div that renders <InfoCard /> and replace the static id with the generated
unique id and add the role attribute.
---
Nitpick comments:
In `@apps/client/src/shared/components/balloon/Balloon.tsx`:
- Around line 19-22: Move the variantStyle constant out of the Balloon component
so it isn't re-created on every render: locate the variantStyle object
(currently defined inside the Balloon component in Balloon.tsx) and hoist it to
module scope above the Balloon function, keeping the same key names ('gray',
'main') and values; ensure the Balloon component continues to reference
variantStyle unchanged.
| return ( | ||
| <div> | ||
| {' '} | ||
| <Balloon variant="main" side="bottom"> | ||
| <div className="text-lg font-semibold">치삐가 방금</div> | ||
|
|
||
| <div className="text-sm opacity-90">도토리 1개를 모았어요!</div> | ||
| </Balloon> | ||
| <Balloon variant="gray" side="left" onClose={() => alert('닫힘')}> | ||
| <div className="text-lg font-semibold">치삐가 방금</div> | ||
|
|
||
| <div className="text-sm opacity-90">도토리 1개를 모았어요!</div> | ||
| </Balloon> | ||
| <Balloon variant="gray" side="left"> | ||
| <Icon name="ic_info" size={16} /> | ||
| <div className="text-sm opacity-90">도토리 1개를 모았어요!</div> | ||
| </Balloon> | ||
| <Balloon variant="main" side="bottom"> | ||
| <div className="flex items-center gap-3"> | ||
| {/* 캐릭터 이미지 */} | ||
| <Icon name="chippi_profile" size={40} /> | ||
|
|
||
| {/* 텍스트 영역 */} | ||
| <div className="flex flex-col"> | ||
| <div className="text-[18px] font-semibold">치삐가 방금</div> | ||
| <div className="text-[16px]">도토리 1개를 모았어요!</div> | ||
| </div> | ||
| </div> | ||
| </Balloon> | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check JobPins router registration and actual usage
echo "=== JobPins 라우터 등록 확인 ==="
rg -n "JobPins" --type=tsx --type=ts -g '!node_modules' -C 3
echo "=== 관심 직무 핀 관련 API 연결 여부 ==="
rg -n "jobPin" --type=tsx --type=ts -g '!node_modules' -C 2Repository: Pinback-Team/pinback-client
Length of output: 198
🏁 Script executed:
#!/bin/bash
# Corrected search - JobPins references in TypeScript/TSX files
echo "=== JobPins 라우터 등록 확인 ==="
rg "JobPins" -g "*.ts" -g "*.tsx" -C 3
echo "=== jobPin 또는 관심 직무 핀 관련 API 연결 여부 ==="
rg "jobPin|job-pin|pin.*job" -g "*.ts" -g "*.tsx" -C 2
echo "=== JobPins 파일 자체 확인 ==="
cat -n apps/client/src/pages/jobPins/JobPins.tsx | head -50Repository: Pinback-Team/pinback-client
Length of output: 7832
JobPins 페이지가 Balloon 컴포넌트 데모 코드로만 구성되어 있고 실제 기능이 없음
현재 파일은 라우터에 등록되어 있고 사이드바에서 "관심 직무 핀" 메뉴로 접근 가능하지만, 콘텐츠는 Balloon 컴포넌트 쇼케이스의 하드코딩된 예시로만 구성되어 있습니다. "치삐가 방금", "도토리 1개를 모았어요!" 등의 텍스트는 실제 데이터가 아니며, API 연결, 데이터 페칭, 상태 관리가 모두 없는 상태입니다. 실제 관심 직무 핀 기능을 구현하거나, 이 페이지의 목적을 명확히 해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/client/src/pages/jobPins/JobPins.tsx` around lines 5 - 35, The JobPins
page currently contains only hardcoded Balloon demo markup; replace this with
real data fetching and state management: add state (e.g., const [pins, setPins]
= useState<JobPin[]>() ) and a data loader (useEffect or React Query/SWR) that
calls your job pins API (e.g., fetchJobPins or getSavedJobPins) to populate
pins, render the list by mapping pins to <Balloon ...> elements instead of the
static examples, show loading/error UI while fetching, implement onClose
handlers that call an unpin API (e.g., removeJobPin or unpinJob) and update
state optimistically (update setPins to remove the pin on success or rollback on
failure), and ensure prop typings for the JobPins component and children so
Balloon receives the correct title, subtitle, icon, and id values from each pin.
| return <div>관심 직무 핀 페이지</div>; | ||
| return ( | ||
| <div> | ||
| {' '} |
There was a problem hiding this comment.
불필요한 {' '} 제거
<div> 직후의 {' '}는 렌더링에 아무런 의미 없는 공백 문자이며, 코드 편집 중 발생한 아티팩트로 보입니다. 제거해 주세요.
🧹 수정 제안
return (
<div>
- {' '}
<Balloon variant="main" side="bottom">📝 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.
| {' '} | |
| return ( | |
| <div> | |
| <Balloon variant="main" side="bottom"> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/client/src/pages/jobPins/JobPins.tsx` at line 7, In the JobPins
component (JobPins.tsx) remove the unnecessary JSX whitespace token {' '} that
appears immediately after the <div> element (an artifact of editing) so the
<div> children render without the extraneous blank node; locate the stray {' '}
near the top of the component and delete it leaving the surrounding JSX intact.
|
|
||
| <div className="text-sm opacity-90">도토리 1개를 모았어요!</div> | ||
| </Balloon> | ||
| <Balloon variant="gray" side="left" onClose={() => alert('닫힘')}> |
There was a problem hiding this comment.
alert() — 프로덕션 코드에 포함하면 안 되는 임시 핸들러
onClose={() => alert('닫힘')}은 명백히 개발 중 사용하는 임시 코드입니다. alert()은 브라우저 UI를 블로킹하며 프로덕션 환경에 적합하지 않습니다. 실제 닫기 로직이 구현될 때까지 핸들러를 제거하거나 TODO 주석으로 명시해 주세요.
🔧 수정 제안
- <Balloon variant="gray" side="left" onClose={() => alert('닫힘')}>
+ {/* TODO: 닫기 상태 관리 로직 구현 후 onClose 연결 필요 */}
+ <Balloon variant="gray" side="left">📝 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.
| <Balloon variant="gray" side="left" onClose={() => alert('닫힘')}> | |
| {/* TODO: 닫기 상태 관리 로직 구현 후 onClose 연결 필요 */} | |
| <Balloon variant="gray" side="left"> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/client/src/pages/jobPins/JobPins.tsx` at line 13, The temporary alert
handler on the Balloon component (onClose={() => alert('닫힘')}) must be removed
or replaced: locate the Balloon usage in JobPins.tsx and either remove the
onClose prop or replace it with a non-blocking stub that clearly marks intent
(e.g., a TODO handler that calls a close function or dispatches a close action)
and add a TODO comment referencing Balloon.onClose to implement real close logic
later; ensure no alert() calls remain in production code.
| {onClose && ( | ||
| <button onClick={onClose}> | ||
| <Icon name="ic_close" size={16} /> | ||
| </button> | ||
| )} |
There was a problem hiding this comment.
닫기 버튼에 aria-label 및 type="button" 속성 누락
onClose 조건부 버튼에 두 가지 문제가 있습니다.
aria-label이 없어 스크린 리더 사용자가 버튼의 목적을 인식할 수 없습니다.type="button"이 없어, 이 컴포넌트가<form>내부에서 사용될 경우 기본값인type="submit"으로 동작하여 의도치 않은 폼 제출이 발생할 수 있습니다.
♿ 수정 제안
- <button onClick={onClose}>
+ <button type="button" aria-label="닫기" onClick={onClose}>
<Icon name="ic_close" size={16} />
</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.
| {onClose && ( | |
| <button onClick={onClose}> | |
| <Icon name="ic_close" size={16} /> | |
| </button> | |
| )} | |
| {onClose && ( | |
| <button type="button" aria-label="닫기" onClick={onClose}> | |
| <Icon name="ic_close" size={16} /> | |
| </button> | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/client/src/shared/components/balloon/Balloon.tsx` around lines 34 - 38,
조건부로 렌더되는 닫기 버튼 (onClose 핸들러와 함께 렌더되는 <button> 바로 그 요소)에 accessibility와 form 동작
문제를 고치기 위해 버튼에 명시적 type="button" 속성을 추가하고 스크린 리더용 aria-label을 추가하세요 (예:
aria-label="닫기" 또는 컴포넌트에 전달되는 레이블 prop 사용). 변경 대상 식별자: onClose 조건부 버튼 렌더링과 Icon
name="ic_close" 사용 부분(Balloon.tsx의 해당 버튼)을 찾아 해당 요소에 type="button"과 적절한
aria-label을 설정하면 됩니다.
📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
사용 방법
shadecn Tooltip이 아닌 Balloon을 새로 만든이유
이번 말풍선 UI는 기존 shadcn Tooltip과 목적과 책임이 다르기 때문에 별도의 Balloon 컴포넌트로 분리하여 구현했습니다. shadcn Tooltip은 hover 또는 focus 기반으로 동작하는 짧은 설명용 보조 UI에 적합하며, 접근성 측면에서 보조 정보를 제공하는 역할을 합니다. 하지만 핀백에서 구현한 말풍선은 이벤트나 상태 변화에 따라 노출되는 안내 UI로 이미지, 다중 텍스트, 서로 다른 폰트 크기, 닫기 버튼 등을 포함하는 보다 복합적인 구조를 포함하고있습니다.
Tooltip과 Balloon은 사용 목적과 복잡도가 다르다고 판단하여 별도의 Balloon 컴포넌트를 도입했습니다.
📷 Screenshot