[FEAT] 파트너 매칭 페이지 구현 및 파트너 저장 API 연동#13
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ 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. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthrough파트너 매칭 페이지를 새로 구현하며, AI 자연어 검색을 통한 파트너 탐색, 애니메이션 테이블을 이용한 다중 선택, 배치 저장 기능을 추가합니다. API 엔드포인트와 새로운 UI 컴포넌트, React Query 훅을 도입하고, 푸터를 멀티 컬럼 레이아웃으로 재설계하며, 앱 라우팅 및 국제화를 업데이트합니다. Changes파트너 매칭 기능 전체 구현
예상 코드 리뷰 난이도🎯 3 (보통) | ⏱️ ~25분 관련 PR
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ 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 |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (4)
src/components/PartnerTable.jsx (2)
135-159: ⚡ Quick win빈 상태 스타일이 중복됩니다.
두 조건부 렌더링 블록(line 136-145, 149-158)이 동일한 스타일 객체를 사용합니다. 스타일 객체를 상수로 추출하면 유지보수성이 향상됩니다.
♻️ 제안된 리팩터링
+const EMPTY_STATE_STYLE = { + padding: "40px 0", + textAlign: "center", + fontSize: 14, + color: "#a2a0a0", +}; + export default function PartnerTable({ partners, isFetching, submittedQuery, checkedIds, onToggleAll, onToggleOne, }) { const { t } = useI18n(); const allChecked = partners.length > 0 && checkedIds.size === partners.length; return ( <div> {/* ... */} {!submittedQuery && ( - <div - style={{ - padding: "40px 0", - textAlign: "center", - fontSize: 14, - color: "#a2a0a0", - }} - > + <div style={EMPTY_STATE_STYLE}> {t("matches_empty_prompt")} </div> )} {!isFetching && submittedQuery && partners.length === 0 && ( - <div - style={{ - padding: "40px 0", - textAlign: "center", - fontSize: 14, - color: "#a2a0a0", - }} - > + <div style={EMPTY_STATE_STYLE}> {t("matches_no_results")} </div> )}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/PartnerTable.jsx` around lines 135 - 159, The two conditional empty-state divs in PartnerTable.jsx (the blocks that check !submittedQuery and !isFetching && submittedQuery && partners.length === 0) duplicate the same inline style object; extract that style into a single constant (e.g., EMPTY_STATE_STYLE) defined once (above the component or at top of the component body) and replace both inline style props with style={EMPTY_STATE_STYLE} to remove duplication and improve maintainability while keeping the same behavior and usage of submittedQuery, isFetching, partners, and t.
110-110: ⚡ Quick win파트너 ID가 없을 때 인덱스를 키로 사용하면 안정성 문제가 발생할 수 있습니다.
_id ?? i패턴은_id가 없을 때 배열 인덱스로 폴백하는데, 이는 항목이 추가/제거/재정렬될 때 React의 재조정 알고리즘을 혼란시켜 잘못된 컴포넌트 상태나 불필요한 리렌더링을 유발할 수 있습니다.파트너 객체에 항상 고유한
_id가 있는지 확인하거나, 없는 경우 안정적인 대체 식별자를 생성하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/PartnerTable.jsx` at line 110, Using the mapping where you set const id = partner._id ?? i in PartnerTable, avoid falling back to the array index because it breaks React keys; instead ensure a stable unique key by using partner._id when present and otherwise generating/storing a persistent identifier (e.g., compute a stable hash from immutable partner fields like email or slug, or generate a UUID/nanoid once and attach it to the partner object or keep it in a useMemo/useRef map keyed by a stable property) so the id used for the key remains unique and stable across reorders and updates.src/pages/Matches.jsx (1)
46-46: 💤 Low value파트너 타입 결정 로직이 다중 태그를 가진 파트너를 고려하지 않을 수 있습니다.
현재
tags?.includes("Buyer")를 사용하여 "buyer"와 "seller"를 구분하는데, 파트너가 "Buyer"와 "Seller" 태그를 모두 가진 경우 항상 "buyer"로 분류됩니다.이것이 의도된 동작인지 확인하거나, 양쪽 타입을 모두 가진 파트너에 대한 명시적인 처리를 추가하는 것을 고려하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/Matches.jsx` at line 46, The partnerType assignment using p.tags?.includes("Buyer") ? "buyer" : "seller" incorrectly forces partners with both "Buyer" and "Seller" tags to "buyer"; update the logic in Matches.jsx where partnerType is set (the expression referencing p.tags) to explicitly detect both tags (e.g., check includes("Buyer") && includes("Seller")) and return an appropriate value or representation (such as "both" or an array/enum) instead of defaulting to "seller", and ensure downstream code that reads partnerType is updated to handle the new "both"/array representation.src/components/SquareButton.jsx (1)
33-33: ⚡ Quick win비활성 상태 커서는
not-allowed를 사용하는 것이 권장됩니다.
disabled상태에서cursor: "default"를 사용하면 사용자에게 버튼을 클릭할 수 없다는 명확한 신호를 주지 못합니다.cursor: "not-allowed"를 사용하면 더 나은 UX를 제공합니다.또한 비활성 버튼에 시각적 피드백(예: 투명도 감소)을 추가하는 것을 고려하세요.
♻️ 제안된 개선사항
borderRadius: 8, width: 200, padding: "12px 0", fontSize: 16, fontWeight: 500, - cursor: disabled ? "default" : "pointer", + cursor: disabled ? "not-allowed" : "pointer", + opacity: disabled ? 0.5 : 1, flexShrink: 0, whiteSpace: "nowrap", ...styleProp,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/SquareButton.jsx` at line 33, The button's disabled style currently uses cursor: "default" which doesn't clearly signal unclickable state; update the style in the SquareButton component where the inline style sets cursor (the expression using the disabled prop) to use cursor: "not-allowed" when disabled, and also add a visual cue such as lowering opacity (e.g., opacity: 0.6 or similar) in the same style object; ensure the change targets the style block/method that returns the JSX styles for SquareButton so all disabled renders show the updated cursor and reduced opacity.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/apis/endpoints.ts`:
- Line 37: The partnerById helper currently interpolates id directly into the
path causing malformed URLs for ids with special characters; update the
partnerById function (partnerById: (id: string) => ...) to URL-encode the id
(e.g., use encodeURIComponent or equivalent) before interpolation so the
returned path is safe for all id values.
In `@src/components/Footer.jsx`:
- Around line 103-123: The three anchor elements around SiInstagram, SiYoutube,
and FaLinkedinIn in Footer.jsx use href="#" which creates non-functional links;
replace each non-destination anchor with an accessible button (preserve onClick
behavior and styles) when there is no real URL, or change href="#" to the actual
external URL or app route when a destination exists, ensure any buttons have
type="button" and the same keyboard/focus behavior and aria-labels as the
original links to maintain accessibility and styling.
In `@src/components/PartnerSearchInput.jsx`:
- Around line 52-68: The input in PartnerSearchInput (the controlled input using
value={query}, onChange={setQuery}, and onKeyDown={handleKeyDown}) lacks an
accessible label; update the input element to include an appropriate aria-label
or aria-labelledby (e.g., use aria-label={t("matches_search_placeholder")} or
point aria-labelledby to a visible label element) so screen readers can describe
the field, ensuring the translation key t("matches_search_placeholder") or a
dedicated label id is used for the accessible name.
In `@src/components/PartnerTable.jsx`:
- Line 108: Remove the inappropriate mode="wait" prop from the AnimatePresence
usage in PartnerTable.jsx (the AnimatePresence component instance inside the
PartnerTable render), because list items should animate concurrently; simply
omit the mode prop (or set it to the default) so AnimatePresence uses its
default behavior for list rendering.
In `@src/components/PartnerTableRow.jsx`:
- Around line 28-61: The clickable square using motion.div in
PartnerTableRow.jsx is not keyboard/screen-reader accessible; replace the
motion.div with an interactive element or add proper ARIA and keyboard handling:
use a semantic button or keep motion.div but add role="checkbox",
aria-checked={checked}, tabIndex={0}, an onKeyDown handler that triggers
onToggle on Enter/Space, and include an accessible label (aria-label or
aria-labelledby) so screen readers announce the control; ensure the onToggle
prop is used for both click and key activation and preserve the existing motion
props/animation on the interactive element.
- Around line 98-113: The anchor is rendering unvalidated websiteUrl which
allows dangerous schemes like "javascript:"; update the PartnerTableRow.jsx
rendering logic to validate websiteUrl before using it as href (e.g., only allow
URLs that start with "http://" or "https://"); if validation fails, render the
partner name as plain text (or a non-interactive <span>) with the same visual
styles instead of an <a>; reference the websiteUrl variable and the JSX that
currently returns the <a> (and keep the displayed {name}) so you modify that
branch to only set href when the scheme check passes.
In `@src/hooks/matches/usePartners.js`:
- Around line 4-9: 요약: usePartnerSearch가 공백 문자열(" ")을 유효값으로 처리해 불필요한 요청을 보냅니다.
수정: usePartnerSearch 내부에서 const trimmed = q?.trim()로 쿼리를 트림하고, queryKey는
["partners","search", trimmed]로 바꾸며 enabled는 !!trimmed로 판정하고 queryFn에는
partnersApi.search({ q: trimmed })를 사용해 트림된 값만 요청 및 캐싱되도록 변경하세요; 관련 심볼:
usePartnerSearch, useQuery, queryKey, queryFn, partnersApi.search, enabled.
In `@src/hooks/matches/useSavePartner.js`:
- Line 16: The early return in saveAll currently returns { saved: 0,
alreadySaved: 0 } when partnerItems.length === 0, which mismatches the normal
return shape that also includes savedIds; update that early return to return {
saved: 0, alreadySaved: 0, savedIds: [] } so callers that destructure savedIds
(e.g., in Matches.jsx) won't break; locate the check on partnerItems in the
saveAll function and add the savedIds: [] field to the returned object.
In `@src/pages/Matches.jsx`:
- Around line 61-71: The toast branching in Matches.jsx uses overlapping
conditions for saved, skipped, and alreadySaved so cases like all three >0 hide
alreadySaved; update the conditional logic in the block that checks saved/
skipped/ alreadySaved (the if/else chain around lines with toast.success and
toast) to be mutually exclusive by first checking the most specific case(s)
(e.g., if saved>0 && skipped>0 && alreadySaved>0), then the two-way combinations
(saved>0 && skipped>0, saved>0 && alreadySaved>0), then single-value cases
(saved>0, alreadySaved>0), and ensure you use toast.success where appropriate
and toast for already-saved-only messages so the displayed message always
includes all relevant counts.
---
Nitpick comments:
In `@src/components/PartnerTable.jsx`:
- Around line 135-159: The two conditional empty-state divs in PartnerTable.jsx
(the blocks that check !submittedQuery and !isFetching && submittedQuery &&
partners.length === 0) duplicate the same inline style object; extract that
style into a single constant (e.g., EMPTY_STATE_STYLE) defined once (above the
component or at top of the component body) and replace both inline style props
with style={EMPTY_STATE_STYLE} to remove duplication and improve maintainability
while keeping the same behavior and usage of submittedQuery, isFetching,
partners, and t.
- Line 110: Using the mapping where you set const id = partner._id ?? i in
PartnerTable, avoid falling back to the array index because it breaks React
keys; instead ensure a stable unique key by using partner._id when present and
otherwise generating/storing a persistent identifier (e.g., compute a stable
hash from immutable partner fields like email or slug, or generate a UUID/nanoid
once and attach it to the partner object or keep it in a useMemo/useRef map
keyed by a stable property) so the id used for the key remains unique and stable
across reorders and updates.
In `@src/components/SquareButton.jsx`:
- Line 33: The button's disabled style currently uses cursor: "default" which
doesn't clearly signal unclickable state; update the style in the SquareButton
component where the inline style sets cursor (the expression using the disabled
prop) to use cursor: "not-allowed" when disabled, and also add a visual cue such
as lowering opacity (e.g., opacity: 0.6 or similar) in the same style object;
ensure the change targets the style block/method that returns the JSX styles for
SquareButton so all disabled renders show the updated cursor and reduced
opacity.
In `@src/pages/Matches.jsx`:
- Line 46: The partnerType assignment using p.tags?.includes("Buyer") ? "buyer"
: "seller" incorrectly forces partners with both "Buyer" and "Seller" tags to
"buyer"; update the logic in Matches.jsx where partnerType is set (the
expression referencing p.tags) to explicitly detect both tags (e.g., check
includes("Buyer") && includes("Seller")) and return an appropriate value or
representation (such as "both" or an array/enum) instead of defaulting to
"seller", and ensure downstream code that reads partnerType is updated to handle
the new "both"/array representation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 948279c8-9fe4-4df4-b298-e68180c37154
⛔ Files ignored due to path filters (3)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlsrc/assets/footer_logo_icon.svgis excluded by!**/*.svgsrc/assets/footer_logo_text.svgis excluded by!**/*.svg
📒 Files selected for processing (15)
package.jsonsrc/App.jsxsrc/apis/endpoints.tssrc/apis/index.tssrc/apis/modules/myBusiness.tssrc/components/Footer.jsxsrc/components/Header.jsxsrc/components/PartnerSearchInput.jsxsrc/components/PartnerTable.jsxsrc/components/PartnerTableRow.jsxsrc/components/SquareButton.jsxsrc/hooks/matches/usePartners.jssrc/hooks/matches/useSavePartner.jssrc/lib/i18n/dict.jssrc/pages/Matches.jsx
| myBusiness: { | ||
| profile: "/my-business/profile", | ||
| partners: "/my-business/partners", | ||
| partnerById: (id: string) => `/my-business/partners/${id}`, |
There was a problem hiding this comment.
partnerById의 id는 URL 인코딩이 필요합니다.
현재는 id를 그대로 붙여 경로를 만들기 때문에, 특수문자 포함 시 잘못된 엔드포인트로 요청될 수 있습니다.
제안 수정안
- partnerById: (id: string) => `/my-business/partners/${id}`,
+ partnerById: (id: string) =>
+ `/my-business/partners/${encodeURIComponent(id)}`,📝 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.
| partnerById: (id: string) => `/my-business/partners/${id}`, | |
| partnerById: (id: string) => | |
| `/my-business/partners/${encodeURIComponent(id)}`, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/apis/endpoints.ts` at line 37, The partnerById helper currently
interpolates id directly into the path causing malformed URLs for ids with
special characters; update the partnerById function (partnerById: (id: string)
=> ...) to URL-encode the id (e.g., use encodeURIComponent or equivalent) before
interpolation so the returned path is safe for all id values.
| <a | ||
| href="#" | ||
| onClick={(e) => e.preventDefault()} | ||
| style={{ display: "flex", color: "#fafafa" }} | ||
| > | ||
| <SiInstagram size={19} /> | ||
| </a> | ||
| <a | ||
| href="#" | ||
| onClick={(e) => e.preventDefault()} | ||
| style={{ display: "flex", color: "#fafafa" }} | ||
| > | ||
| <SiYoutube size={21} /> | ||
| </a> | ||
| <a | ||
| href="#" | ||
| onClick={(e) => e.preventDefault()} | ||
| style={{ display: "flex", color: "#fafafa" }} | ||
| > | ||
| <FaLinkedinIn size={19} /> | ||
| </a> |
There was a problem hiding this comment.
동작하지 않는 앵커 링크(href="#")가 사용자 경험을 해칩니다.
현재는 클릭 가능한 링크처럼 보이지만 실제 이동이 없어 혼란을 줍니다. 목적지가 없으면 button으로 바꾸고, 목적지가 있으면 실제 URL/라우트로 연결해 주세요.
Also applies to: 280-291
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/Footer.jsx` around lines 103 - 123, The three anchor elements
around SiInstagram, SiYoutube, and FaLinkedinIn in Footer.jsx use href="#" which
creates non-functional links; replace each non-destination anchor with an
accessible button (preserve onClick behavior and styles) when there is no real
URL, or change href="#" to the actual external URL or app route when a
destination exists, ensure any buttons have type="button" and the same
keyboard/focus behavior and aria-labels as the original links to maintain
accessibility and styling.
| <input | ||
| value={query} | ||
| onChange={(e) => setQuery(e.target.value)} | ||
| onKeyDown={handleKeyDown} | ||
| placeholder={t("matches_search_placeholder")} | ||
| style={{ | ||
| flex: 1, | ||
| border: "none", | ||
| outline: "none", | ||
| background: "transparent", | ||
| fontSize: 16, | ||
| color: "#080616", | ||
| lineHeight: "24px", | ||
| padding: 0, | ||
| height: 24, | ||
| }} | ||
| /> |
There was a problem hiding this comment.
검색 입력 필드에 접근성 레이블이 없습니다.
입력 필드에 시각적 레이블이나 aria-label 속성이 없어 스크린 리더 사용자가 입력 필드의 목적을 이해하기 어렵습니다.
aria-label 또는 aria-labelledby 속성을 추가하여 접근성을 개선하세요.
♿ 제안된 수정사항
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={t("matches_search_placeholder")}
+ aria-label={t("matches_search_placeholder")}
style={{
flex: 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.
| <input | |
| value={query} | |
| onChange={(e) => setQuery(e.target.value)} | |
| onKeyDown={handleKeyDown} | |
| placeholder={t("matches_search_placeholder")} | |
| style={{ | |
| flex: 1, | |
| border: "none", | |
| outline: "none", | |
| background: "transparent", | |
| fontSize: 16, | |
| color: "#080616", | |
| lineHeight: "24px", | |
| padding: 0, | |
| height: 24, | |
| }} | |
| /> | |
| <input | |
| value={query} | |
| onChange={(e) => setQuery(e.target.value)} | |
| onKeyDown={handleKeyDown} | |
| placeholder={t("matches_search_placeholder")} | |
| aria-label={t("matches_search_placeholder")} | |
| style={{ | |
| flex: 1, | |
| border: "none", | |
| outline: "none", | |
| background: "transparent", | |
| fontSize: 16, | |
| color: "#080616", | |
| lineHeight: "24px", | |
| padding: 0, | |
| height: 24, | |
| }} | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/PartnerSearchInput.jsx` around lines 52 - 68, The input in
PartnerSearchInput (the controlled input using value={query},
onChange={setQuery}, and onKeyDown={handleKeyDown}) lacks an accessible label;
update the input element to include an appropriate aria-label or aria-labelledby
(e.g., use aria-label={t("matches_search_placeholder")} or point aria-labelledby
to a visible label element) so screen readers can describe the field, ensuring
the translation key t("matches_search_placeholder") or a dedicated label id is
used for the accessible name.
| </div> | ||
| </div> | ||
|
|
||
| <AnimatePresence mode="wait"> |
There was a problem hiding this comment.
AnimatePresence mode="wait" 속성이 리스트 렌더링에 부적합합니다.
mode="wait"는 이전 요소가 완전히 사라진 후 다음 요소를 표시하는데, 여러 항목을 동시에 애니메이션해야 하는 리스트에는 적합하지 않습니다. 이로 인해 항목이 순차적으로 하나씩만 나타나는 문제가 발생할 수 있습니다.
리스트의 경우 mode 속성을 제거하거나 명시적으로 기본값을 사용하세요.
🔧 제안된 수정사항
- <AnimatePresence mode="wait">
+ <AnimatePresence>
{partners.map((partner, i) => {📝 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.
| <AnimatePresence mode="wait"> | |
| <AnimatePresence> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/PartnerTable.jsx` at line 108, Remove the inappropriate
mode="wait" prop from the AnimatePresence usage in PartnerTable.jsx (the
AnimatePresence component instance inside the PartnerTable render), because list
items should animate concurrently; simply omit the mode prop (or set it to the
default) so AnimatePresence uses its default behavior for list rendering.
| <a | ||
| href={websiteUrl} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| style={{ | ||
| fontSize: 12, | ||
| color: "#080616", | ||
| textDecoration: "underline", | ||
| overflow: "hidden", | ||
| textOverflow: "ellipsis", | ||
| whiteSpace: "nowrap", | ||
| cursor: "pointer", | ||
| }} | ||
| > | ||
| {name} | ||
| </a> |
There was a problem hiding this comment.
외부 링크 URL 검증 없이 렌더링되고 있습니다.
websiteUrl이 비신뢰 데이터일 경우 javascript: 같은 스킴이 들어와 클릭 시 스크립트 실행 위험이 있습니다. http/https만 허용하고 아니면 텍스트로 렌더링하세요.
제안 수정안
export default function PartnerTableRow({
@@
}) {
+ const safeWebsiteUrl =
+ typeof websiteUrl === "string" && /^https?:\/\//i.test(websiteUrl)
+ ? websiteUrl
+ : null;
@@
- <a
- href={websiteUrl}
- target="_blank"
- rel="noopener noreferrer"
- style={{
- fontSize: 12,
- color: "#080616",
- textDecoration: "underline",
- overflow: "hidden",
- textOverflow: "ellipsis",
- whiteSpace: "nowrap",
- cursor: "pointer",
- }}
- >
- {name}
- </a>
+ {safeWebsiteUrl ? (
+ <a
+ href={safeWebsiteUrl}
+ target="_blank"
+ rel="noopener noreferrer"
+ style={{
+ fontSize: 12,
+ color: "#080616",
+ textDecoration: "underline",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ cursor: "pointer",
+ }}
+ >
+ {name}
+ </a>
+ ) : (
+ <span
+ style={{
+ fontSize: 12,
+ color: "#080616",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ }}
+ >
+ {name}
+ </span>
+ )}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/PartnerTableRow.jsx` around lines 98 - 113, The anchor is
rendering unvalidated websiteUrl which allows dangerous schemes like
"javascript:"; update the PartnerTableRow.jsx rendering logic to validate
websiteUrl before using it as href (e.g., only allow URLs that start with
"http://" or "https://"); if validation fails, render the partner name as plain
text (or a non-interactive <span>) with the same visual styles instead of an
<a>; reference the websiteUrl variable and the JSX that currently returns the
<a> (and keep the displayed {name}) so you modify that branch to only set href
when the scheme check passes.
| }); | ||
|
|
||
| const saveAll = async (partnerItems) => { | ||
| if (partnerItems.length === 0) return { saved: 0, alreadySaved: 0 }; |
There was a problem hiding this comment.
Early return이 반환 타입과 일치하지 않습니다.
saveAll 함수가 빈 배열일 때 { saved: 0, alreadySaved: 0 }를 반환하지만, 정상 경로에서는 savedIds 배열도 포함됩니다(line 30). 호출하는 쪽(src/pages/Matches.jsx line 55)에서 savedIds를 구조 분해하므로 일관성 있게 포함해야 합니다.
🐛 제안된 수정사항
const saveAll = async (partnerItems) => {
- if (partnerItems.length === 0) return { saved: 0, alreadySaved: 0 };
+ if (partnerItems.length === 0) return { saved: 0, alreadySaved: 0, savedIds: [] };
const results = await Promise.allSettled(📝 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.
| if (partnerItems.length === 0) return { saved: 0, alreadySaved: 0 }; | |
| if (partnerItems.length === 0) return { saved: 0, alreadySaved: 0, savedIds: [] }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/hooks/matches/useSavePartner.js` at line 16, The early return in saveAll
currently returns { saved: 0, alreadySaved: 0 } when partnerItems.length === 0,
which mismatches the normal return shape that also includes savedIds; update
that early return to return { saved: 0, alreadySaved: 0, savedIds: [] } so
callers that destructure savedIds (e.g., in Matches.jsx) won't break; locate the
check on partnerItems in the saveAll function and add the savedIds: [] field to
the returned object.
| if (saved > 0 && skipped === 0 && alreadySaved === 0) { | ||
| toast.success(`${saved}개 파트너가 저장되었습니다.`); | ||
| } else if (saved > 0 && skipped > 0) { | ||
| toast.success(`${saved}개 저장 완료 (웹 결과 ${skipped}개 제외)`); | ||
| } else if (saved > 0 && alreadySaved > 0) { | ||
| toast.success( | ||
| `${saved}개 저장 완료 (${alreadySaved}개는 이미 저장된 파트너)`, | ||
| ); | ||
| } else if (alreadySaved > 0) { | ||
| toast(`이미 저장된 파트너입니다.`); | ||
| } |
There was a problem hiding this comment.
토스트 메시지 조건이 중복되어 일부 경우를 올바르게 처리하지 못합니다.
saved, skipped, alreadySaved가 모두 0보다 큰 경우, line 63의 조건(saved > 0 && skipped > 0)이 먼저 매칭되어 alreadySaved 정보가 사용자에게 표시되지 않습니다.
조건들을 상호 배타적으로 만들거나, 모든 케이스를 명시적으로 처리하는 것을 권장합니다.
🐛 제안된 수정사항
- if (saved > 0 && skipped === 0 && alreadySaved === 0) {
+ if (saved > 0 && skipped === 0 && alreadySaved === 0) {
toast.success(`${saved}개 파트너가 저장되었습니다.`);
- } else if (saved > 0 && skipped > 0) {
- toast.success(`${saved}개 저장 완료 (웹 결과 ${skipped}개 제외)`);
- } else if (saved > 0 && alreadySaved > 0) {
+ } else if (saved > 0 && skipped > 0 && alreadySaved === 0) {
+ toast.success(`${saved}개 저장 완료 (웹 결과 ${skipped}개 제외)`);
+ } else if (saved > 0 && skipped === 0 && alreadySaved > 0) {
toast.success(
`${saved}개 저장 완료 (${alreadySaved}개는 이미 저장된 파트너)`,
);
+ } else if (saved > 0 && skipped > 0 && alreadySaved > 0) {
+ toast.success(
+ `${saved}개 저장 완료 (웹 결과 ${skipped}개, 중복 ${alreadySaved}개 제외)`,
+ );
- } else if (alreadySaved > 0) {
+ } else if (saved === 0 && alreadySaved > 0) {
toast(`이미 저장된 파트너입니다.`);
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/Matches.jsx` around lines 61 - 71, The toast branching in
Matches.jsx uses overlapping conditions for saved, skipped, and alreadySaved so
cases like all three >0 hide alreadySaved; update the conditional logic in the
block that checks saved/ skipped/ alreadySaved (the if/else chain around lines
with toast.success and toast) to be mutually exclusive by first checking the
most specific case(s) (e.g., if saved>0 && skipped>0 && alreadySaved>0), then
the two-way combinations (saved>0 && skipped>0, saved>0 && alreadySaved>0), then
single-value cases (saved>0, alreadySaved>0), and ensure you use toast.success
where appropriate and toast for already-saved-only messages so the displayed
message always includes all relevant counts.
🔍 PR 요약
/matches) UI 전체 구현 및 파트너 저장 API 연동🧾 관련 이슈
🛠️ 주요 변경 사항
PartnerTable/PartnerTableRow컴포넌트 분리POST /my-business/partners파트너 저장 API 연동hooks/matches/폴더 구조로 훅 정리,myBusinessApi모듈 추가🧠 의도 및 배경
Summary by CodeRabbit
릴리스 노트
새로운 기능
Chores