Skip to content

[feature] 어드민 링크 추가 모바일 편집 페이지 구현#1781

Merged
suhyun113 merged 22 commits into
develop-fefrom
feature/#1761-admin-basic-info-link-MOA-1006
Jun 30, 2026
Merged

[feature] 어드민 링크 추가 모바일 편집 페이지 구현#1781
suhyun113 merged 22 commits into
develop-fefrom
feature/#1761-admin-basic-info-link-MOA-1006

Conversation

@suhyun113

@suhyun113 suhyun113 commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈

#1761

📝작업 내용

링크 추가 모바일 편집 페이지(LinkEditPage)를 구현하였습니다.

변경 사항

  • LinkEditPage 구현: 인스타그램·유튜브 링크를 편집하는 모바일 서브 페이지. FreeTagEditPage와 동일하게 activePage state 기반으로 전환되며 별도 라우트를 사용하지 않습니다.
  • LinkField 컴포넌트 추가: URL 입력 전용 필드 컴포넌트. EditField를 그대로 래핑하고 내부에 <input type="url">을 배치하는 방식으로, 기존 TextField와 동일한 구조를 따릅니다. label 색상 커스터마이징을 위해 EditFieldlabelColor prop을 추가하였습니다.
  • LinkField 위치: editFields/의 공용 컴포넌트가 아닌 LinkEditPage/ 폴더 내에 위치시켰습니다. LinkEditPage에서만 사용하는 전용 컴포넌트이기 때문입니다.
  • 링크 유효성 검사: 기존 validateSocialLink 유틸을 재사용하여 입력마다 실시간으로 검사합니다. 저장 시 오류가 있으면 저장을 막습니다.

화면

링크 입력된 상태 링크 미입력 상태 링크 형식 오류
image image image

중점적으로 리뷰받고 싶은 부분(선택)

LinkField 설계 방식

TextField와 구조가 유사하지만 별도 컴포넌트로 분리하였습니다.

  • TextField<textarea> 기반으로 자동 높이 조절 로직(useLayoutEffect)이 포함되어 있고, LinkField<input type="url"> 기반의 단일 라인 입력입니다.
  • URL 입력 시 텍스트 색상이 accent[1][900](#3DBBFF)으로 변하는 동작과 에러 표시가 추가로 필요하여 별도 컴포넌트로 구현하였습니다.
  • TextFieldtype prop을 추가해 분기하는 방안도 검토했으나, 조건부 useLayoutEffect와 타입별 전용 prop이 생겨 컴포넌트가 복잡해지는 문제로 기각하였습니다.

🫡 참고사항

  • 플레이스홀더·라벨 텍스트는 데스크탑과 동일하게 SNS_CONFIG 상수를 사용합니다.
  • 저장 시 유효성 오류가 있으면 alert로 안내 후 저장을 막습니다. (데스크탑의 helperText 방식과는 달리 모바일에서는 카드 하단에 에러 메시지를 표시합니다.)

Summary by CodeRabbit

  • New Features

    • 모바일 관리자 정보 편집에서 링크 추가 화면이 새로 제공되어, 인스타그램/유튜브 링크를 별도 화면에서 편집할 수 있습니다.
    • 링크 입력 시 실시간 검증, 오류 표시, 지우기 버튼, 저장 전 오류 차단이 적용됩니다.
    • 모바일에서 링크 개수 표시와 화면 전환 흐름이 개선되었습니다.
  • Bug Fixes

    • 링크 저장 시 입력값이 올바르지 않으면 경고 후 저장을 막아 잘못된 링크 등록을 방지합니다.
    • 링크 일부만 수정해도 기존 값과 자연스럽게 합쳐져 반영됩니다.

@suhyun113 suhyun113 removed their assignment Jun 23, 2026
@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
moadong Ready Ready Preview, Comment Jun 30, 2026 3:59pm

@suhyun113 suhyun113 added ✨ Feature 기능 개발 💻 FE Frontend labels Jun 23, 2026
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@suhyun113, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 55 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e890c428-e3a6-4869-bccb-a29c4122254a

📥 Commits

Reviewing files that changed from the base of the PR and between 9c1e9e4 and 3a3f142.

📒 Files selected for processing (1)
  • frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/hooks/useClubInfoEdit.ts

Walkthrough

ClubInfoEditTabMobile'links' 서브페이지를 추가하고, LinkEditPageLinkField 컴포넌트를 신규 구현했다. useClubInfoEdit 훅에 handleUpdateClubWithLinks 콜백을 추가하고, EditFieldlabelColor prop을 확장했다.

Changes

모바일 링크 편집 페이지 추가

Layer / File(s) Summary
EditField labelColor prop 확장
frontend/src/pages/AdminPage/components/editFields/EditField/EditField.styles.ts, frontend/src/pages/AdminPage/components/editFields/EditField/EditField.tsx
Label styled 컴포넌트에 $color?: string prop을 추가하고, EditFieldPropslabelColor?: string을 노출해 동적 라벨 색상을 지원한다.
LinkField 컴포넌트 구현
frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkField.styles.ts, .../LinkField.tsx
ContentRow, Input($hasValue 색상 분기), ClearButton, ErrorMessage 스타일을 정의하고, 포커스 기반 클리어 버튼·에러 메시지 표시를 담당하는 LinkField 컴포넌트를 신규 구현한다.
LinkEditPage 컴포넌트 구현
frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkEditPage.styles.ts, .../LinkEditPage.tsx
links/errors 상태 관리, validateSocialLink 기반 검증, onSave→onSaveToServer→onBack 저장 흐름, dirty 기반 버튼 비활성화를 포함한 LinkEditPage를 신규 구현한다.
useClubInfoEdit에 handleUpdateClubWithLinks 추가
frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/hooks/useClubInfoEdit.ts
socialLinks를 병합해 updateClub을 호출하는 handleUpdateClubWithLinks 콜백을 추가하고, 실패 시 알림 메시지와 함께 훅 반환값에 노출한다.
ClubInfoEditTabMobile 'links' 서브페이지 연결
frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx, .../ClubInfoEditTabMobile.tsx
ActivePage 타입에 'links'를 추가하고, LinkEditPage 렌더 분기·SNS 링크 카운트 로직·네비게이션 핸들러를 연결하며, ClubInfoEditTab에서 handleUpdateClubWithLinksonSocialLinksChange를 전달한다.
모바일 링크 편집 기능 문서 추가
frontend/docs/features/admin/info/mobile-link-edit.md
activePage 기반 서브뷰 전환, LinkField 동작 규칙, validateSocialLink 검증 및 저장 차단 흐름을 정리한 문서를 추가한다.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Moadong/moadong#400: snsConfig(SNS_CONFIG)와 validateSocialLink를 공유하며, 이 PR의 링크 검증·저장 흐름이 해당 인프라를 직접 활용한다.
  • Moadong/moadong#874: 동일한 ClubInfoEditTab.tsx에서 모바일 소셜 링크 업데이트 핸들러 연결 지점이 겹친다.
  • Moadong/moadong#1732: EditFieldEditField.styles.tsLabel 스타일을 동일하게 수정·확장한 변경이다.

Suggested reviewers

  • oesnuj
  • seongwon030
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 어드민에서 링크 추가용 모바일 편집 페이지를 구현한 변경 사항을 정확히 요약합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/#1761-admin-basic-info-link-MOA-1006

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.

@gemini-code-assist gemini-code-assist 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.

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: '' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

현재 errors 상태가 빈 문자열로 초기화되어 있습니다. 만약 기존에 저장되어 있던 링크에 이미 유효성 오류가 있는 상태에서 진입하고, 사용자가 다른 필드만 수정하여 저장하려고 할 때 기존 오류가 감지되지 않고 그대로 저장될 수 있는 버그가 존재합니다.

초기 렌더링 시에도 initialLinks 값을 기준으로 유효성 검사를 수행하여 errors 상태를 초기화하는 것이 안전합니다.

Suggested change
const [errors, setErrors] = useState({ instagram: '', youtube: '' });
const [errors, setErrors] = useState({
instagram: validateSocialLink('instagram', initialLinks.instagram),
youtube: validateSocialLink('youtube', initialLinks.youtube),
});

Comment on lines +57 to 59
const snsLinkCount = (['instagram', 'youtube'] as const).filter(
(key) => socialLinks[key].trim() !== '',
).length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

socialLinks 객체의 특정 플랫폼 키가 존재하지 않거나 undefined일 경우, trim() 메서드를 호출할 때 런타임 에러(TypeError: Cannot read properties of undefined (reading 'trim'))가 발생할 수 있습니다.

안전한 실행을 위해 옵셔널 체이닝(?.)을 사용하는 것이 좋습니다.

Suggested change
const snsLinkCount = (['instagram', 'youtube'] as const).filter(
(key) => socialLinks[key].trim() !== '',
).length;
const snsLinkCount = (['instagram', 'youtube'] as const).filter(
(key) => socialLinks[key]?.trim() !== '',
).length;

Comment on lines +75 to +78
initialLinks={{
instagram: socialLinks.instagram,
youtube: socialLinks.youtube,
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

socialLinks.instagram 또는 socialLinks.youtube 값이 undefined이거나 null일 경우, LinkEditPage 내부의 LinkField 컴포넌트(input 태그)에 undefinedvalue로 전달되어 React에서 제어 컴포넌트(Controlled Component) 관련 경고가 발생할 수 있습니다.

안전하게 빈 문자열('')을 기본값으로 지정하여 전달하는 것을 권장합니다.

Suggested change
initialLinks={{
instagram: socialLinks.instagram,
youtube: socialLinks.youtube,
}}
initialLinks={{
instagram: socialLinks.instagram || '',
youtube: socialLinks.youtube || '',
}}

@seongwon030 seongwon030 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

수고하셨습니다 ! 링크 추가되었을 때 숫자로만 보이는데 어떤 링크가 추가되었는지 잘 모르겠네요.
instagram링크인지 youtube링크인지는 보여야 할 것 같아요.

Image

}));
};

const handleSave = () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

여기서도 바로 저장이 안되고 기본정보수정페이지에서 저장해야 하네여

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

자유태그 페이지에서 답글 단 것처럼 똑같이 수정해두겠습니다! 1e1704b

@suhyun113

Copy link
Copy Markdown
Collaborator Author

수고하셨습니다 ! 링크 추가되었을 때 숫자로만 보이는데 어떤 링크가 추가되었는지 잘 모르겠네요. instagram링크인지 youtube링크인지는 보여야 할 것 같아요.

Image

저도 동의하는 부분인데, 현재는 링크 추가와 자유태그 추가 모두 클릭하여 탭을 하나 더 이동하는 구조라 UX 적으로 조금 개선이 되어야할 것 같다는 생각이 들어요.
추가된 링크 개수만 나오는게 현재의 디자인이라 일단 그렇게 구현했는데, 디자이너분들과 함께 한번 이야기해보면 좋을 것 같습니다.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 to src/* 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6f0d5c3 and 1e1704b.

📒 Files selected for processing (11)
  • frontend/docs/features/admin/info/mobile-link-edit.md
  • frontend/src/pages/AdminPage/components/MobileSaveButtonArea/MobileSaveButtonArea.tsx
  • frontend/src/pages/AdminPage/components/editFields/EditField/EditField.styles.ts
  • frontend/src/pages/AdminPage/components/editFields/EditField/EditField.tsx
  • frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx
  • frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTabMobile.tsx
  • frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkEditPage.styles.ts
  • frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkEditPage.tsx
  • frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkField.styles.ts
  • frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkField.tsx
  • frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/hooks/useClubInfoEdit.ts

Comment on lines +27 to +50
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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
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`를
기준으로 로직을 정리하면 됩니다.

Comment on lines +31 to +59
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>}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.

Comment on lines +175 to +197
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}`);
},
},
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

실패해도 링크가 저장된 것처럼 상태가 굳습니다.

setInitialValues를 mutation 전에 갱신해서 updateClub가 실패해도 socialLinks와 기준값이 같아집니다. 현재 모바일 흐름은 로컬 상태를 먼저 바꾸고 바로 뒤로 돌아가기 때문에, 실패 후에도 메인 화면에서는 저장된 것처럼 보이고 isDirtyfalse로 떨어져 재시도 동선도 사라집니다.

💡 제안
-    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.

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

Base automatically changed from feature/#1760-admin-basic-info-free-tag-MOA-1005 to develop-fe June 30, 2026 13:49
@reviewer-harry

reviewer-harry Bot commented Jun 30, 2026

Copy link
Copy Markdown

Claude finished @suhyun113's task in 3m 51s —— View job


harry 리뷰 (synchronize)

이번 푸시는 develop-fe를 머지한 커밋(9c1e9e4a) 하나예요. 머지 충돌 해소 과정에서 useClubInfoEdit.ts가 깨졌어요. 🚨

  • handleUpdateClubWithLinks 함수 안에 handleUpdateClubWithTags가 중첩 정의되면서 링크 저장용 updateClub 호출이 통째로 사라졌어요. 모바일 링크 편집 저장이 서버에 반영되지 않아요.
  • updateClub 객체에 tags/introduction/presidentName/presidentPhoneNumber/socialLinks 키가 중복으로 들어가 있고, onError alert도 중복이에요.
  • 중괄호가 맞지 않아 타입체크/빌드가 깨질 가능성이 높아요.

이 PR의 핵심 기능(링크 저장)이 동작하지 않는 상태라 머지 전에 꼭 정리가 필요해요. 자세한 수정 예시는 인라인 코멘트에 남겼어요. Fix this →

나머지 머지 변경분(EditField.styles.ts의 폰트/자간 정리, ClubInfoEditTab*handleUpdateClubWithTags 연결 등)은 develop-fe에서 넘어온 변경이라 이번 리뷰 범위에서 따로 짚지 않았어요. 그 부분과 충돌 정리만 끝나면 깔끔할 것 같아요. 👍


  • 리뷰 기준(.github/harry-review-rules.md) 확인
  • 새 커밋 변경분 확인 (1e1704bd..9c1e9e4a)
  • 인라인 코멘트 작성
  • 총평 작성

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown

🎨 UI 변경사항을 확인해주세요

변경된 스토리를 Chromatic에서 확인해주세요.

구분 링크
🔍 변경사항 리뷰 https://www.chromatic.com/build?appId=67904e61c16daa99a63b44a7&number=676
📖 Storybook https://67904e61c16daa99a63b44a7-eqhearyotp.chromatic.com/

9개 스토리 변경 · 전체 68개 스토리 · 26개 컴포넌트

Comment on lines +168 to +177
const handleUpdateClubWithLinks = (newLinks: {
instagram: string;
youtube: string;
}) => {
if (!clubDetail || !clubDetail.id) return;

const mergedLinks = { ...socialLinks, ...newLinks };
setInitialValues((prev) =>
prev ? { ...prev, socialLinks: mergedLinks } : null,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

develop-fe 머지 충돌 해소가 잘못 들어간 것 같아요. 지금 handleUpdateClubWithLinks가 열린 채로 바로 안쪽에 handleUpdateClubWithTags가 중첩 정의돼 있고, 정작 링크 저장용 updateClub 호출은 사라졌어요. 그래서 모바일 링크 편집에서 저장해도 setInitialValues만 돌고 서버로는 아무것도 안 보내져요.

추가로 아래쪽 updateClub 객체에 tags·introduction·presidentName·presidentPhoneNumber·socialLinks 키가 두 번씩 중복으로 들어가 있어요(socialLinks: mergedLinks 뒤에 socialLinks: socialLinks가 덮어써서 머지된 링크가 무시돼요). onErroralert도 두 줄로 중복돼 있고요. 게다가 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을 갱신하도록 따로 손보면 좋아요.

@reviewer-harry

reviewer-harry Bot commented Jun 30, 2026

Copy link
Copy Markdown

Claude finished @suhyun113's task in 1m 1s —— View job


harry 리뷰 (synchronize)

이번 푸시(3a3f1428)는 직전에 제가 짚었던 develop-fe 머지 충돌 깨짐을 정리한 커밋이에요. git diff 9c1e9e4a..3a3f1428 기준으로 useClubInfoEdit.ts 한 파일만 확인했어요.

  • handleUpdateClubWithLinks(168–198)와 handleUpdateClubWithTags(200–224)가 깔끔하게 분리됐어요. 중첩 정의가 사라졌어요. ✅
  • 링크 저장용 updateClub 호출이 복구됐어요. 이제 모바일 링크 편집 저장이 서버로 정상 전송돼요. ✅
  • socialLinks 중복 key와 중복 alert가 모두 제거됐어요(링크는 링크 저장 실패, 태그는 자유태그 저장 실패로 메시지도 알맞게 분기됐어요). ✅
  • 중괄호도 맞아서 타입체크/빌드가 깨질 문제는 없어 보여요. ✅

제가 지난번에 요청드린 그대로 잘 정리해 주셔서 이번 커밋은 통과예요. 수고하셨어요! 👍

참고로 CodeRabbit이 짚은 "updateClub 실패 시 setInitialValues가 먼저 굳어 저장된 것처럼 보이는 문제"는 이번 머지 정리와 별개라 이번 범위에서는 다시 짚지 않았어요. 여유 있을 때 두 핸들러 모두 onSuccess에서 baseline을 갱신하도록 손보면 더 좋아요.


  • 리뷰 기준(.github/harry-review-rules.md) 확인
  • 새 커밋 변경분 확인 (9c1e9e4a..3a3f1428)
  • 충돌 정리 검증 (핸들러 분리·중복 key/alert 제거)
  • 총평 작성

@suhyun113 suhyun113 merged commit 0817f35 into develop-fe Jun 30, 2026
8 checks passed
@suhyun113 suhyun113 deleted the feature/#1761-admin-basic-info-link-MOA-1006 branch June 30, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FE Frontend ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants