Skip to content

[FEAT] 파트너 매칭 페이지 구현 및 파트너 저장 API 연동#13

Merged
BaeSeong-min merged 19 commits into
developfrom
feat/#11
May 10, 2026
Merged

[FEAT] 파트너 매칭 페이지 구현 및 파트너 저장 API 연동#13
BaeSeong-min merged 19 commits into
developfrom
feat/#11

Conversation

@BaeSeong-min

@BaeSeong-min BaeSeong-min commented May 10, 2026

Copy link
Copy Markdown
Contributor

🔍 PR 요약

  • 파트너 매칭 페이지(/matches) UI 전체 구현 및 파트너 저장 API 연동

🧾 관련 이슈

🛠️ 주요 변경 사항

  • 히어로 섹션, AI 자연어 검색 인풋, 파트너 테이블 UI 구현
  • PartnerTable / PartnerTableRow 컴포넌트 분리
  • 전체/개별 체크박스 선택 기능 및 상태 리프트업
  • POST /my-business/partners 파트너 저장 API 연동
  • Framer Motion 스태거 등장 애니메이션 및 체크박스 마이크로 인터랙션 추가
  • 다크 글래스모피즘 Footer 컴포넌트 구현 및 조건부 렌더링 적용
  • hooks/matches/ 폴더 구조로 훅 정리, myBusinessApi 모듈 추가

🧠 의도 및 배경

  • 파트너 매칭 페이지가 기존에는 미구현 상태로 임시 UI만 존재했습니다. 따라서 AI 기반 자연어 검색으로 적합한 파트너를 추천받고, 원하는 파트너를 즉시 저장할 수 있는 플로우 구현를 구현하였습니다.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 파트너 검색 및 선택 기능 추가
    • Footer를 소셜 링크, 연락처, 네비게이션이 포함된 다단계 레이아웃으로 개선
    • 파트너 추가 기능 구현
    • 다국어 지원 확대 (영어, 한국어)
  • Chores

    • 애니메이션 및 아이콘 라이브러리 의존성 추가

Review Change Stack

@BaeSeong-min BaeSeong-min self-assigned this May 10, 2026
@vercel

vercel Bot commented May 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
k-statra-frontend Ready Ready Preview, Comment May 10, 2026 6:48am

@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@BaeSeong-min has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 31 minutes and 10 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6fec0797-dd4e-4cbe-999b-88ea5b82006b

📥 Commits

Reviewing files that changed from the base of the PR and between f8c132e and 0fdf6d0.

📒 Files selected for processing (3)
  • src/components/PartnerTable.jsx
  • src/components/PartnerTableRow.jsx
  • src/hooks/matches/usePartners.js
📝 Walkthrough

Walkthrough

파트너 매칭 페이지를 새로 구현하며, AI 자연어 검색을 통한 파트너 탐색, 애니메이션 테이블을 이용한 다중 선택, 배치 저장 기능을 추가합니다. API 엔드포인트와 새로운 UI 컴포넌트, React Query 훅을 도입하고, 푸터를 멀티 컬럼 레이아웃으로 재설계하며, 앱 라우팅 및 국제화를 업데이트합니다.

Changes

파트너 매칭 기능 전체 구현

레이어 / 파일 요약
API 계약 및 의존성
package.json, src/apis/endpoints.ts, src/apis/index.ts, src/apis/modules/myBusiness.ts
framer-motionreact-icons 의존성 추가. ENDPOINTS.myBusiness에 profile, partners, partnerById 경로 정의. myBusinessApi 내보내기로 파트너 프로필 조회, 파트너 목록 조회(페이지네이션 지원), 파트너 저장, 파트너 삭제 메서드 제공.
데이터 조회 및 변경 훅
src/hooks/matches/usePartners.js, src/hooks/matches/useSavePartner.js
usePartnerSearch(q, options): 검색어 기반 파트너 조회를 위한 React Query 훅. useSavePartner(): 다중 파트너 병렬 저장, HTTP 409 중복 감지, 집계된 결과({ saved, alreadySaved, savedIds }) 반환.
UI 컴포넌트: 검색 및 테이블
src/components/PartnerSearchInput.jsx, src/components/PartnerTable.jsx, src/components/PartnerTableRow.jsx, src/components/SquareButton.jsx
검색 입력 컴포넌트는 Enter 또는 버튼 클릭으로 제출. 파트너 테이블은 애니메이션 행 진입/퇴출, 전체 선택/개별 선택 토글, 공상태/결과없음 메시지 표시. 테이블 행은 체크박스, 아바타, 웹사이트 링크, 국가/산업/프로필 정보 표시. SquareButton에 disabledstyleProp 지원 추가.
푸터 재설계
src/components/Footer.jsx
간단한 링크 모음에서 멀티 컬럼 레이아웃으로 전환. 좌측에 로고, 소셜 아이콘(Instagram/YouTube/LinkedIn), 네비게이션 컬럼(NAV_COLUMNS 맵핑), 연락처 섹션(이메일/전화/주소 with 루시드 아이콘), 구분선, 언어별 바닥 링크 포함.
페이지 및 라우팅 통합
src/pages/Matches.jsx, src/App.jsx, src/components/Header.jsx
Matches 페이지: 텍스트 기반 submittedQuery, checkedIds/hiddenIds Set 관리, 파트너 필터링, 저장/삭제 처리 로직으로 완전히 재작성. App.jsx: 주 컨테이너 클래스에 /matches 경로 추가, 푸터 조건부 렌더링(/, /login, /register, /payments* 제외). Header: nav_partner_matching 항목을 /partners에서 /matches로 변경.
국제화
src/lib/i18n/dict.js
영어(en) 및 한국어(ko) 사전에 파트너 매칭 UI 문자열 추가: 배지, 제목, 부제목, 검색 자리표시자, 추가 버튼 레이블, 테이블 컬럼(회사/국가/산업/프로필), 공상태/결과없음 메시지.

예상 코드 리뷰 난이도

🎯 3 (보통) | ⏱️ ~25분

관련 PR

  • K-Strata/frontend#8: 두 PR 모두 중앙화된 API 계층(endpoints, apis/index)과 파트너 관련 API 모듈을 다루고 있어 관련성이 있습니다.

Poem

🐰 토끼의 파트너 매칭 축시

파트너 찾는 길을 열고,
애니메이션 테이블이 춤을 춥니다.
검색어 한 줄에 반짝이는 결과들,
선택하고 저장하면 완성되는 매칭,
멋진 푸터와 함께 아름다운 여정! 🌟

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive 추가 의존성 설치(framer-motion, react-icons) 및 Footer의 상당한 리디자인 등이 기본 요구사항 범위를 벗어나는지 명확하지 않습니다. framer-motion과 react-icons 추가가 사전 승인되었는지, Footer 리디자인이 의도된 범위인지 검토해주시기 바랍니다.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 주요 변경사항인 파트너 매칭 페이지 구현 및 파트너 저장 API 연동을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명은 템플릿의 필수 섹션(요약, 관련 이슈, 주요 변경사항, 의도 및 배경)을 모두 포함하고 있으며 충분히 상세합니다.
Linked Issues check ✅ Passed 모든 코드 변경사항이 이슈 #11의 요구사항을 충족합니다: MatchesPage 생성, 히어로 섹션, 검색 입력, PartnerTableRow, Footer 구현, API 연동 모두 완료되었습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#11

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@BaeSeong-min BaeSeong-min changed the title [#11] 파트너 매칭 페이지 구현 및 파트너 저장 API 연동 [FEAT] 파트너 매칭 페이지 구현 및 파트너 저장 API 연동 May 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 72bee49 and f8c132e.

⛔ Files ignored due to path filters (3)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • src/assets/footer_logo_icon.svg is excluded by !**/*.svg
  • src/assets/footer_logo_text.svg is excluded by !**/*.svg
📒 Files selected for processing (15)
  • package.json
  • src/App.jsx
  • src/apis/endpoints.ts
  • src/apis/index.ts
  • src/apis/modules/myBusiness.ts
  • src/components/Footer.jsx
  • src/components/Header.jsx
  • src/components/PartnerSearchInput.jsx
  • src/components/PartnerTable.jsx
  • src/components/PartnerTableRow.jsx
  • src/components/SquareButton.jsx
  • src/hooks/matches/usePartners.js
  • src/hooks/matches/useSavePartner.js
  • src/lib/i18n/dict.js
  • src/pages/Matches.jsx

Comment thread src/apis/endpoints.ts
myBusiness: {
profile: "/my-business/profile",
partners: "/my-business/partners",
partnerById: (id: string) => `/my-business/partners/${id}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

partnerByIdid는 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.

Suggested change
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.

Comment thread src/components/Footer.jsx
Comment on lines +103 to +123
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

동작하지 않는 앵커 링크(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.

Comment on lines +52 to +68
<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,
}}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

검색 입력 필드에 접근성 레이블이 없습니다.

입력 필드에 시각적 레이블이나 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.

Suggested change
<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">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
<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.

Comment thread src/components/PartnerTableRow.jsx
Comment on lines +98 to +113
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

외부 링크 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.

Comment thread src/hooks/matches/usePartners.js
});

const saveAll = async (partnerItems) => {
if (partnerItems.length === 0) return { saved: 0, alreadySaved: 0 };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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.

Comment thread src/pages/Matches.jsx
Comment on lines +61 to 71
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(`이미 저장된 파트너입니다.`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

토스트 메시지 조건이 중복되어 일부 경우를 올바르게 처리하지 못합니다.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 파트너 매칭 페이지 구현

1 participant