[feature] 어드민 링크 추가 모바일 편집 페이지 구현#1781
Conversation
- 기본 css dashed는 점선 간격 조정 안 되는 한계가 있어서 svg로 구현
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 55 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
Walkthrough
Changes모바일 링크 편집 페이지 추가
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ 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.
Code Review
This pull request introduces a mobile-specific link editing page (LinkEditPage and LinkField) to allow administrators to edit Instagram and YouTube links on mobile devices. It also updates ClubInfoEditTabMobile to support switching to the new 'links' subview and enhances EditField with a customizable label color. The review feedback highlights several improvement opportunities to prevent runtime errors and React warnings: initializing the validation errors state with the initial link values to catch pre-existing invalid inputs, using optional chaining when trimming social links to avoid potential null pointer exceptions, and providing fallback empty strings for link values to prevent React controlled component warnings.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
|
||
| const LinkEditPage = ({ initialLinks, onSave, onBack }: LinkEditPageProps) => { | ||
| const [links, setLinks] = useState(initialLinks); | ||
| const [errors, setErrors] = useState({ instagram: '', youtube: '' }); |
There was a problem hiding this comment.
현재 errors 상태가 빈 문자열로 초기화되어 있습니다. 만약 기존에 저장되어 있던 링크에 이미 유효성 오류가 있는 상태에서 진입하고, 사용자가 다른 필드만 수정하여 저장하려고 할 때 기존 오류가 감지되지 않고 그대로 저장될 수 있는 버그가 존재합니다.
초기 렌더링 시에도 initialLinks 값을 기준으로 유효성 검사를 수행하여 errors 상태를 초기화하는 것이 안전합니다.
| const [errors, setErrors] = useState({ instagram: '', youtube: '' }); | |
| const [errors, setErrors] = useState({ | |
| instagram: validateSocialLink('instagram', initialLinks.instagram), | |
| youtube: validateSocialLink('youtube', initialLinks.youtube), | |
| }); |
| const snsLinkCount = (['instagram', 'youtube'] as const).filter( | ||
| (key) => socialLinks[key].trim() !== '', | ||
| ).length; |
There was a problem hiding this comment.
socialLinks 객체의 특정 플랫폼 키가 존재하지 않거나 undefined일 경우, trim() 메서드를 호출할 때 런타임 에러(TypeError: Cannot read properties of undefined (reading 'trim'))가 발생할 수 있습니다.
안전한 실행을 위해 옵셔널 체이닝(?.)을 사용하는 것이 좋습니다.
| const snsLinkCount = (['instagram', 'youtube'] as const).filter( | |
| (key) => socialLinks[key].trim() !== '', | |
| ).length; | |
| const snsLinkCount = (['instagram', 'youtube'] as const).filter( | |
| (key) => socialLinks[key]?.trim() !== '', | |
| ).length; |
| initialLinks={{ | ||
| instagram: socialLinks.instagram, | ||
| youtube: socialLinks.youtube, | ||
| }} |
There was a problem hiding this comment.
socialLinks.instagram 또는 socialLinks.youtube 값이 undefined이거나 null일 경우, LinkEditPage 내부의 LinkField 컴포넌트(input 태그)에 undefined가 value로 전달되어 React에서 제어 컴포넌트(Controlled Component) 관련 경고가 발생할 수 있습니다.
안전하게 빈 문자열('')을 기본값으로 지정하여 전달하는 것을 권장합니다.
| initialLinks={{ | |
| instagram: socialLinks.instagram, | |
| youtube: socialLinks.youtube, | |
| }} | |
| initialLinks={{ | |
| instagram: socialLinks.instagram || '', | |
| youtube: socialLinks.youtube || '', | |
| }} |
| })); | ||
| }; | ||
|
|
||
| const handleSave = () => { |
There was a problem hiding this comment.
여기서도 바로 저장이 안되고 기본정보수정페이지에서 저장해야 하네여
There was a problem hiding this comment.
자유태그 페이지에서 답글 단 것처럼 똑같이 수정해두겠습니다! 1e1704b
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTabMobile.tsx (1)
14-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win새 import도
@/별칭으로 맞춰주세요.이 경로 규칙에서는 상대 import를 늘리기보다
@/*별칭을 쓰도록 맞추는 편이 일관됩니다. 이번에 추가한FreeTagEditPage/LinkEditPage도 같은 기준으로 두는 게 좋겠습니다.♻️ 제안
-import FreeTagEditPage from './components/mobile/FreeTagEditPage/FreeTagEditPage'; -import LinkEditPage from './components/mobile/LinkEditPage/LinkEditPage'; +import FreeTagEditPage from '`@/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/FreeTagEditPage/FreeTagEditPage`'; +import LinkEditPage from '`@/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkEditPage`';As per coding guidelines,
frontend/src/**/*.{tsx,ts}: Use@/*path alias to map tosrc/*in imports.🤖 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 `@frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTabMobile.tsx` around lines 14 - 15, The newly added imports in ClubInfoEditTabMobile should use the `@/` path alias instead of relative paths to match the project’s import convention. Update the FreeTagEditPage and LinkEditPage import statements in ClubInfoEditTabMobile so they resolve through `@/`* consistently with the rest of the codebase.Source: Coding guidelines
🤖 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
`@frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkEditPage.tsx`:
- Around line 27-50: `LinkEditPage`의 저장 검증이 변경된 필드만 반영해서 초기 서버값의 오류를 놓치고 있습니다.
`useState`로 만드는 `errors` 초기값부터 `initialLinks`를 기준으로 `validateSocialLink` 결과를
채우고, `handleSave`에서는 현재 `links` 전체를 다시 검증해 `hasErrors`를 판단하도록 수정하세요.
`handleChange`, `handleSave`, `validateSocialLink`, `initialLinks`, `links`를
기준으로 로직을 정리하면 됩니다.
In
`@frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkField.tsx`:
- Around line 31-59: The input in LinkField is missing an accessible name
because the EditField label is not associated with the Styled.Input, so update
the LinkField/Styled.Input wiring to connect the label and field with
id/htmlFor, or add a fallback aria-label using the existing label prop. Make the
fix in LinkField where EditField and Styled.Input are rendered so assistive
technologies can identify the field correctly without relying on placeholder
text.
In `@frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/hooks/useClubInfoEdit.ts`:
- Around line 175-197: `useClubInfoEdit` updates `setInitialValues` before
`updateClub`, which makes failed saves look persisted and clears `isDirty`. Move
the `setInitialValues`/local baseline update to the `updateClub` success path
(or only after a confirmed mutation result), and keep the current `onError` path
from mutating the saved baseline so `socialLinks` remains retryable when
`updateClub` fails.
---
Nitpick comments:
In `@frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTabMobile.tsx`:
- Around line 14-15: The newly added imports in ClubInfoEditTabMobile should use
the `@/` path alias instead of relative paths to match the project’s import
convention. Update the FreeTagEditPage and LinkEditPage import statements in
ClubInfoEditTabMobile so they resolve through `@/`* consistently with the rest of
the codebase.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3c66b97d-3d4a-42cb-a7b0-dbd1fc1e9c89
📒 Files selected for processing (11)
frontend/docs/features/admin/info/mobile-link-edit.mdfrontend/src/pages/AdminPage/components/MobileSaveButtonArea/MobileSaveButtonArea.tsxfrontend/src/pages/AdminPage/components/editFields/EditField/EditField.styles.tsfrontend/src/pages/AdminPage/components/editFields/EditField/EditField.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTabMobile.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkEditPage.styles.tsfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkEditPage.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkField.styles.tsfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkField.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/hooks/useClubInfoEdit.ts
| const [links, setLinks] = useState(initialLinks); | ||
| const [errors, setErrors] = useState({ instagram: '', youtube: '' }); | ||
|
|
||
| const isDirty = | ||
| links.instagram !== initialLinks.instagram || | ||
| links.youtube !== initialLinks.youtube; | ||
|
|
||
| const handleChange = (key: keyof LinkEditPageLinks, value: string) => { | ||
| setLinks((prev) => ({ ...prev, [key]: value })); | ||
| setErrors((prev) => ({ | ||
| ...prev, | ||
| [key]: validateSocialLink(key, value), | ||
| })); | ||
| }; | ||
|
|
||
| const handleSave = () => { | ||
| const hasErrors = Object.values(errors).some((e) => e !== ''); | ||
| if (hasErrors) { | ||
| alert('링크에 오류가 있어요. 수정 후 다시 시도해주세요!'); | ||
| return; | ||
| } | ||
| onSave(links); | ||
| onSaveToServer(links); | ||
| onBack(); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
초기 링크는 저장 전에 다시 검증해야 합니다.
errors를 빈 값으로 시작하고 변경된 필드만 검증해서, 서버에서 내려온 기존 링크가 이미 잘못된 형식이어도 사용자가 그 필드를 건드리지 않으면 저장이 통과합니다. 이 PR의 “오류가 있으면 저장 차단” 계약을 지키려면 초기값으로도 에러를 계산하고, handleSave에서 현재 links를 한 번 더 검증하는 쪽이 맞습니다.
수정 예시
+const validateLinks = (nextLinks: LinkEditPageLinks) => ({
+ instagram: validateSocialLink('instagram', nextLinks.instagram),
+ youtube: validateSocialLink('youtube', nextLinks.youtube),
+});
+
const LinkEditPage = ({
initialLinks,
onSave,
onSaveToServer,
onBack,
}: LinkEditPageProps) => {
const [links, setLinks] = useState(initialLinks);
- const [errors, setErrors] = useState({ instagram: '', youtube: '' });
+ const [errors, setErrors] = useState(() => validateLinks(initialLinks));
@@
const handleSave = () => {
- const hasErrors = Object.values(errors).some((e) => e !== '');
+ const nextErrors = validateLinks(links);
+ setErrors(nextErrors);
+ const hasErrors = Object.values(nextErrors).some(Boolean);
if (hasErrors) {
alert('링크에 오류가 있어요. 수정 후 다시 시도해주세요!');
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.
| const [links, setLinks] = useState(initialLinks); | |
| const [errors, setErrors] = useState({ instagram: '', youtube: '' }); | |
| const isDirty = | |
| links.instagram !== initialLinks.instagram || | |
| links.youtube !== initialLinks.youtube; | |
| const handleChange = (key: keyof LinkEditPageLinks, value: string) => { | |
| setLinks((prev) => ({ ...prev, [key]: value })); | |
| setErrors((prev) => ({ | |
| ...prev, | |
| [key]: validateSocialLink(key, value), | |
| })); | |
| }; | |
| const handleSave = () => { | |
| const hasErrors = Object.values(errors).some((e) => e !== ''); | |
| if (hasErrors) { | |
| alert('링크에 오류가 있어요. 수정 후 다시 시도해주세요!'); | |
| return; | |
| } | |
| onSave(links); | |
| onSaveToServer(links); | |
| onBack(); | |
| const validateLinks = (nextLinks: LinkEditPageLinks) => ({ | |
| instagram: validateSocialLink('instagram', nextLinks.instagram), | |
| youtube: validateSocialLink('youtube', nextLinks.youtube), | |
| }); | |
| const LinkEditPage = ({ | |
| initialLinks, | |
| onSave, | |
| onSaveToServer, | |
| onBack, | |
| }: LinkEditPageProps) => { | |
| const [links, setLinks] = useState(initialLinks); | |
| const [errors, setErrors] = useState(() => validateLinks(initialLinks)); | |
| const isDirty = | |
| links.instagram !== initialLinks.instagram || | |
| links.youtube !== initialLinks.youtube; | |
| const handleChange = (key: keyof LinkEditPageLinks, value: string) => { | |
| setLinks((prev) => ({ ...prev, [key]: value })); | |
| setErrors((prev) => ({ | |
| ...prev, | |
| [key]: validateSocialLink(key, value), | |
| })); | |
| }; | |
| const handleSave = () => { | |
| const nextErrors = validateLinks(links); | |
| setErrors(nextErrors); | |
| const hasErrors = Object.values(nextErrors).some(Boolean); | |
| if (hasErrors) { | |
| alert('링크에 오류가 있어요. 수정 후 다시 시도해주세요!'); | |
| return; | |
| } | |
| onSave(links); | |
| onSaveToServer(links); | |
| onBack(); |
🤖 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
`@frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkEditPage.tsx`
around lines 27 - 50, `LinkEditPage`의 저장 검증이 변경된 필드만 반영해서 초기 서버값의 오류를 놓치고 있습니다.
`useState`로 만드는 `errors` 초기값부터 `initialLinks`를 기준으로 `validateSocialLink` 결과를
채우고, `handleSave`에서는 현재 `links` 전체를 다시 검증해 `hasErrors`를 판단하도록 수정하세요.
`handleChange`, `handleSave`, `validateSocialLink`, `initialLinks`, `links`를
기준으로 로직을 정리하면 됩니다.
| return ( | ||
| <div> | ||
| <EditField | ||
| label={label} | ||
| isActive={isActive} | ||
| labelColor={colors.gray[800]} | ||
| > | ||
| <Styled.ContentRow> | ||
| <Styled.Input | ||
| type='url' | ||
| value={value} | ||
| placeholder={placeholder} | ||
| $hasValue={value.length > 0} | ||
| onChange={(e) => onChange(e.target.value)} | ||
| onFocus={() => setIsActive(true)} | ||
| onBlur={() => setIsActive(false)} | ||
| /> | ||
| {isActive && value.length > 0 && ( | ||
| <Styled.ClearButton | ||
| type='button' | ||
| onMouseDown={handleClear} | ||
| aria-label='지우기' | ||
| > | ||
| <FieldClearButtonIcon /> | ||
| </Styled.ClearButton> | ||
| )} | ||
| </Styled.ContentRow> | ||
| </EditField> | ||
| {error && <Styled.ErrorMessage>{error}</Styled.ErrorMessage>} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
입력 필드에 접근 가능한 이름을 연결해주세요.
지금 구조에서는 EditField의 라벨 텍스트와 input이 연결되지 않아 보조기기 기준으로 이름 없는 필드가 됩니다. placeholder로는 대체되지 않으니, id/htmlFor를 연결한 실제 라벨을 쓰거나 최소한 aria-label={label}은 넣어두는 편이 안전합니다.
🤖 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
`@frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkField.tsx`
around lines 31 - 59, The input in LinkField is missing an accessible name
because the EditField label is not associated with the Styled.Input, so update
the LinkField/Styled.Input wiring to connect the label and field with
id/htmlFor, or add a fallback aria-label using the existing label prop. Make the
fix in LinkField where EditField and Styled.Input are rendered so assistive
technologies can identify the field correctly without relying on placeholder
text.
| setInitialValues((prev) => | ||
| prev ? { ...prev, socialLinks: mergedLinks } : null, | ||
| ); | ||
|
|
||
| updateClub( | ||
| { | ||
| id: clubDetail.id, | ||
| name: clubName, | ||
| category: selectedCategory, | ||
| division: selectedDivision, | ||
| tags: clubTags, | ||
| introduction: introduction, | ||
| presidentName: clubPresidentName, | ||
| presidentPhoneNumber: telephoneNumber, | ||
| socialLinks: mergedLinks, | ||
| description: clubDetail.description, | ||
| }, | ||
| { | ||
| onError: (error) => { | ||
| alert(`링크 저장에 실패했습니다: ${error.message}`); | ||
| }, | ||
| }, | ||
| ); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
실패해도 링크가 저장된 것처럼 상태가 굳습니다.
setInitialValues를 mutation 전에 갱신해서 updateClub가 실패해도 socialLinks와 기준값이 같아집니다. 현재 모바일 흐름은 로컬 상태를 먼저 바꾸고 바로 뒤로 돌아가기 때문에, 실패 후에도 메인 화면에서는 저장된 것처럼 보이고 isDirty가 false로 떨어져 재시도 동선도 사라집니다.
💡 제안
- setInitialValues((prev) =>
- prev ? { ...prev, socialLinks: mergedLinks } : null,
- );
-
updateClub(
{
id: clubDetail.id,
name: clubName,
category: selectedCategory,
@@
},
{
+ onSuccess: () => {
+ setInitialValues((prev) =>
+ prev ? { ...prev, socialLinks: mergedLinks } : null,
+ );
+ },
onError: (error) => {
alert(`링크 저장에 실패했습니다: ${error.message}`);
},
},
);📝 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.
| setInitialValues((prev) => | |
| prev ? { ...prev, socialLinks: mergedLinks } : null, | |
| ); | |
| updateClub( | |
| { | |
| id: clubDetail.id, | |
| name: clubName, | |
| category: selectedCategory, | |
| division: selectedDivision, | |
| tags: clubTags, | |
| introduction: introduction, | |
| presidentName: clubPresidentName, | |
| presidentPhoneNumber: telephoneNumber, | |
| socialLinks: mergedLinks, | |
| description: clubDetail.description, | |
| }, | |
| { | |
| onError: (error) => { | |
| alert(`링크 저장에 실패했습니다: ${error.message}`); | |
| }, | |
| }, | |
| ); | |
| updateClub( | |
| { | |
| id: clubDetail.id, | |
| name: clubName, | |
| category: selectedCategory, | |
| division: selectedDivision, | |
| tags: clubTags, | |
| introduction: introduction, | |
| presidentName: clubPresidentName, | |
| presidentPhoneNumber: telephoneNumber, | |
| socialLinks: mergedLinks, | |
| description: clubDetail.description, | |
| }, | |
| { | |
| onSuccess: () => { | |
| setInitialValues((prev) => | |
| prev ? { ...prev, socialLinks: mergedLinks } : null, | |
| ); | |
| }, | |
| onError: (error) => { | |
| alert(`링크 저장에 실패했습니다: ${error.message}`); | |
| }, | |
| }, | |
| ); |
🤖 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 `@frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/hooks/useClubInfoEdit.ts`
around lines 175 - 197, `useClubInfoEdit` updates `setInitialValues` before
`updateClub`, which makes failed saves look persisted and clears `isDirty`. Move
the `setInitialValues`/local baseline update to the `updateClub` success path
(or only after a confirmed mutation result), and keep the current `onError` path
from mutating the saved baseline so `socialLinks` remains retryable when
`updateClub` fails.
|
Claude finished @suhyun113's task in 3m 51s —— View job harry 리뷰 (synchronize)이번 푸시는
이 PR의 핵심 기능(링크 저장)이 동작하지 않는 상태라 머지 전에 꼭 정리가 필요해요. 자세한 수정 예시는 인라인 코멘트에 남겼어요. Fix this → 나머지 머지 변경분(
|
🎨 UI 변경사항을 확인해주세요
9개 스토리 변경 · 전체 68개 스토리 · 26개 컴포넌트 |
| const handleUpdateClubWithLinks = (newLinks: { | ||
| instagram: string; | ||
| youtube: string; | ||
| }) => { | ||
| if (!clubDetail || !clubDetail.id) return; | ||
|
|
||
| const mergedLinks = { ...socialLinks, ...newLinks }; | ||
| setInitialValues((prev) => | ||
| prev ? { ...prev, socialLinks: mergedLinks } : null, | ||
| ); |
There was a problem hiding this comment.
develop-fe 머지 충돌 해소가 잘못 들어간 것 같아요. 지금 handleUpdateClubWithLinks가 열린 채로 바로 안쪽에 handleUpdateClubWithTags가 중첩 정의돼 있고, 정작 링크 저장용 updateClub 호출은 사라졌어요. 그래서 모바일 링크 편집에서 저장해도 setInitialValues만 돌고 서버로는 아무것도 안 보내져요.
추가로 아래쪽 updateClub 객체에 tags·introduction·presidentName·presidentPhoneNumber·socialLinks 키가 두 번씩 중복으로 들어가 있어요(socialLinks: mergedLinks 뒤에 socialLinks: socialLinks가 덮어써서 머지된 링크가 무시돼요). onError의 alert도 두 줄로 중복돼 있고요. 게다가 handleUpdateClubWithLinks의 닫는 중괄호가 빠져서 타입체크/빌드가 깨질 거예요.
아래처럼 두 핸들러를 분리해 주세요. (이 블록부터 handleUpdateClubWithTags의 끝 };까지를 교체하는 형태예요)
const handleUpdateClubWithLinks = (newLinks: {
instagram: string;
youtube: string;
}) => {
if (!clubDetail || !clubDetail.id) return;
const mergedLinks = { ...socialLinks, ...newLinks };
setInitialValues((prev) =>
prev ? { ...prev, socialLinks: mergedLinks } : null,
);
updateClub(
{
id: clubDetail.id,
name: clubName,
category: selectedCategory,
division: selectedDivision,
tags: clubTags,
introduction: introduction,
presidentName: clubPresidentName,
presidentPhoneNumber: telephoneNumber,
socialLinks: mergedLinks,
description: clubDetail.description,
},
{
onError: (error) => {
alert(`링크 저장에 실패했습니다: ${error.message}`);
},
},
);
};
const handleUpdateClubWithTags = (newTags: string[]) => {
if (!clubDetail || !clubDetail.id) return;
setInitialValues((prev) => (prev ? { ...prev, clubTags: newTags } : null));
updateClub(
{
id: clubDetail.id,
name: clubName,
category: selectedCategory,
division: selectedDivision,
tags: newTags,
introduction: introduction,
presidentName: clubPresidentName,
presidentPhoneNumber: telephoneNumber,
socialLinks: socialLinks,
description: clubDetail.description,
},
{
onError: (error) => {
alert(`자유태그 저장에 실패했습니다: ${error.message}`);
},
},
);
};CodeRabbit가 앞서 짚은 "실패 시
setInitialValues가 먼저 굳는 문제"는 이 머지 정리와는 별개라 위 예시엔 반영하지 않았어요. 그건 두 핸들러 모두onSuccess에서 baseline을 갱신하도록 따로 손보면 좋아요.
|
Claude finished @suhyun113's task in 1m 1s —— View job harry 리뷰 (synchronize)이번 푸시(
제가 지난번에 요청드린 그대로 잘 정리해 주셔서 이번 커밋은 통과예요. 수고하셨어요! 👍
|


#️⃣연관된 이슈
#1761
📝작업 내용
링크 추가 모바일 편집 페이지(
LinkEditPage)를 구현하였습니다.변경 사항
LinkEditPage구현: 인스타그램·유튜브 링크를 편집하는 모바일 서브 페이지.FreeTagEditPage와 동일하게activePagestate 기반으로 전환되며 별도 라우트를 사용하지 않습니다.LinkField컴포넌트 추가: URL 입력 전용 필드 컴포넌트.EditField를 그대로 래핑하고 내부에<input type="url">을 배치하는 방식으로, 기존TextField와 동일한 구조를 따릅니다.label색상 커스터마이징을 위해EditField에labelColorprop을 추가하였습니다.LinkField위치:editFields/의 공용 컴포넌트가 아닌LinkEditPage/폴더 내에 위치시켰습니다.LinkEditPage에서만 사용하는 전용 컴포넌트이기 때문입니다.validateSocialLink유틸을 재사용하여 입력마다 실시간으로 검사합니다. 저장 시 오류가 있으면 저장을 막습니다.화면
중점적으로 리뷰받고 싶은 부분(선택)
LinkField설계 방식TextField와 구조가 유사하지만 별도 컴포넌트로 분리하였습니다.TextField는<textarea>기반으로 자동 높이 조절 로직(useLayoutEffect)이 포함되어 있고,LinkField는<input type="url">기반의 단일 라인 입력입니다.accent[1][900](#3DBBFF)으로 변하는 동작과 에러 표시가 추가로 필요하여 별도 컴포넌트로 구현하였습니다.TextField에typeprop을 추가해 분기하는 방안도 검토했으나, 조건부useLayoutEffect와 타입별 전용 prop이 생겨 컴포넌트가 복잡해지는 문제로 기각하였습니다.🫡 참고사항
SNS_CONFIG상수를 사용합니다.alert로 안내 후 저장을 막습니다. (데스크탑의helperText방식과는 달리 모바일에서는 카드 하단에 에러 메시지를 표시합니다.)Summary by CodeRabbit
New Features
Bug Fixes