[feature] 위로가기 아이콘 교체 및 스크롤 업 시 플로팅 버튼 노출 - #1828
Conversation
- ScrollToTopButton을 FloatingButtonGroup으로 교체 - 스크롤 다운 시 위로가기 버튼, 스크롤 업 시 공유 버튼 함께 노출 - 두 버튼을 wrapper로 묶어 우측 12px 고정 위치에 배치
…wn alias로 변수명 명확화,
- useShare 훅으로 플랫폼별 공유 로직 (RN WebView / Web Share API / 클립보드) 추출 - ShareButton에서 useShare 재사용 - FloatingButtonGroup 공유 버튼 클릭 시 현재 페이지 URL 공유
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Walkthrough공유 로직을 Changes공유 처리 흐름
스크롤 기반 플로팅 버튼
앱 연결 및 Storybook
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant App
participant FloatingButtonGroup
participant useScrollTrigger
participant useScrollTo
participant useShare
participant ShareTarget
App->>FloatingButtonGroup: 플로팅 버튼 그룹 렌더링
FloatingButtonGroup->>useScrollTrigger: 스크롤 상태 조회
FloatingButtonGroup->>useScrollTo: 상단 이동 요청
FloatingButtonGroup->>useShare: 현재 제목과 URL 전달
useShare->>ShareTarget: WebView·Web Share·클립보드 공유
Possibly related PRs
Suggested labels: 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 |
✅ UI 변경사항 없음
전체 91개 스토리 · 33개 컴포넌트 |
There was a problem hiding this comment.
Code Review
This pull request introduces a mobile-responsive layout for the club introduction edit tab, refactors application form grouping from semester-based to active/inactive status, and replaces the scroll-to-top button with a unified floating button group (scroll and share). Feedback focuses on fixing a bug in the useClubIntroEdit hook where isDirty incorrectly evaluates to true on mount due to auto-generated FAQ IDs, resolving a layout spacing issue in the floating button group when buttons are hidden, and simplifying character limit checks by passing maxLength directly to the textareas.
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.
I am having trouble creating individual review comments. Click here to see my feedback.
frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/hooks/useClubIntroEdit.ts (35-55)
useClubIntroEdit 훅에서 isDirty를 판단할 때 JSON.stringify(faqs)와 JSON.stringify(initialState.faqs)를 비교하고 있습니다. 하지만 FAQEditor 컴포넌트 마운트 시 id가 없는 FAQ 항목에 대해 자동으로 id를 생성하여 onChange를 호출하게 됩니다.
만약 서버에서 처음 받아온 FAQ 데이터에 id가 없다면, 마운트 직후 faqs에는 id가 추가되고 initialState.faqs에는 id가 없어 isDirty가 사용자의 조작 없이도 즉시 true로 변경되는 버그가 발생합니다.
이를 방지하기 위해, 데이터를 처음 불러와 상태를 초기화하는 useClubIntroEdit 훅의 useEffect 내부에서 미리 id를 생성하여 faqs와 initialState.faqs가 동일한 id를 공유하도록 수정하는 것을 권장합니다.
useEffect(() => {
if (clubDetail?.description && clubDetail.id !== loadedClubId) {
const desc = clubDetail.description;
const initialFaqs = (desc.faqs || []).map((faq) => ({
...faq,
id: faq.id || `faq-${Date.now()}-${Math.random()}`,
}));
const initial = {
introDescription: desc.introDescription || '',
activityDescription: desc.activityDescription || '',
awards: desc.awards || [],
idealCandidate: desc.idealCandidate || { tags: [], content: '' },
benefits: desc.benefits || '',
faqs: initialFaqs,
};
setIntroDescription(initial.introDescription);
setActivityDescription(initial.activityDescription);
setAwards(initial.awards);
setIdealCandidate(initial.idealCandidate);
setBenefits(initial.benefits);
setFaqs(initialFaqs);
setInitialState(initial);
setLoadedClubId(clubDetail.id);
}
}, [clubDetail, loadedClubId]);frontend/src/components/common/FloatingButtonGroup/FloatingButtonGroup.styles.ts (16-27)
FloatingButton 컴포넌트에서 $isVisible이 false일 때 visibility: hidden을 사용하고 있습니다. 하지만 이 버튼들은 flex-direction: column과 gap: 8px이 적용된 GroupContainer 내부의 자식 요소입니다. visibility: hidden은 화면에서 보이지 않지만 레이아웃 공간은 여전히 차지하므로, 버튼이 숨겨졌을 때 불필요한 빈 공간(여백)이 생기게 됩니다.
이를 해결하기 위해 $isVisible에 따라 height와 opacity를 함께 트랜지션하고 overflow: hidden을 적용하면, 숨겨졌을 때 레이아웃 공간을 차지하지 않으면서 자연스러운 애니메이션 효과를 유지할 수 있습니다.
export const FloatingButton = styled.button<{ $isVisible: boolean }>`
width: 33px;
height: ${({ $isVisible }) => ($isVisible ? '33px' : '0')};
padding: 0;
background: none;
border: none;
cursor: pointer;
opacity: ${({ $isVisible }) => ($isVisible ? 1 : 0)};
visibility: ${({ $isVisible }) => ($isVisible ? 'visible' : 'hidden')};
overflow: hidden;
transition:
height 0.3s,
opacity 0.3s,
visibility 0.3s;
frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsx (104-109)
TextArea 컴포넌트에 maxLength 속성을 직접 전달하지 않아, 글자 수 제한을 위해 handleIntroChange 등의 커스텀 핸들러에서 매번 길이를 수동으로 체크하고 있습니다.
Styled.TextArea에 maxLength 속성을 직접 전달하면 브라우저가 네이티브하게 글자 수 입력을 제한하므로, 복잡한 수동 길이 체크 핸들러들을 모두 제거하고 인라인 onChange로 단순화할 수 있어 코드가 훨씬 깔끔해집니다. (동일한 패턴을 activityDescription, idealCandidate, benefits 입력 영역에도 적용하고 관련 커스텀 핸들러들을 제거할 수 있습니다.)
<Styled.TextArea
ref={introRef}
value={introDescription}
onChange={(e) => setIntroDescription(e.target.value)}
placeholder={INTRO_DESCRIPTION_PLACEHOLDER}
maxLength={INTRO_DESCRIPTION_MAX}
/>
frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/FAQSection/FAQSection.tsx (51-57)
Styled.AnswerTextarea에 maxLength 속성을 직접 전달하지 않아 handleAnswerChange 핸들러에서 수동으로 길이를 체크하고 있습니다. maxLength={FAQ_ANSWER_MAX}를 직접 전달하면 브라우저가 네이티브하게 입력을 제한하므로, 수동 체크 핸들러를 제거하고 인라인 onChange로 단순화할 수 있습니다.
<Styled.AnswerTextarea
ref={answerRef}
value={faq.answer}
onChange={(e) => onChange(index, 'answer', e.target.value)}
placeholder={FAQ_ANSWER_PLACEHOLDER}
maxLength={FAQ_ANSWER_MAX}
rows={1}
/>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/pages/AdminPage/components/MobileSaveButtonArea/MobileSaveButtonArea.styles.ts (1)
6-14: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win배경색 제거로 하단 고정 버튼 영역이 투명해질 수 있습니다.
SaveButtonArea는 화면 하단에 고정된 오버레이인데, 이전에 있던 흰색background선언이 제거되어 버튼을 감싸는 패딩 영역(상 10px, 좌우 20px)으로 스크롤 중인 페이지 콘텐츠가 비쳐 보일 수 있습니다. 의도된 변경이 아니라면 배경을 다시 추가해주세요.🎨 제안하는 수정
export const SaveButtonArea = styled.div` position: fixed; bottom: 0; left: 50%; transform: translateX(-50%); width: 100%; max-width: 500px; padding: 10px 20px calc(20px + env(safe-area-inset-bottom)); + background: ${colors.base.white}; z-index: ${Z_INDEX.clubDetailFooter};🤖 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/components/MobileSaveButtonArea/MobileSaveButtonArea.styles.ts` around lines 6 - 14, Restore an opaque white background on the SaveButtonArea styled component so the fixed footer’s padding area does not reveal scrolling content behind it.
🧹 Nitpick comments (2)
frontend/src/App.tsx (1)
4-4: 🎯 Functional Correctness | 🔵 Trivial
FloatingButtonGroup이 관리자 라우트에도 전역 노출됩니다.
AppRoutes바깥, 라우터 최상위에 마운트되어 있어/admin/*등 관리자 페이지에서도 공개용 공유/스크롤 버튼이 그대로 노출됩니다. PR 설명에서 이미 후속 논의 대상으로 언급된 사항이라 차단 이슈는 아니지만, 관리자 라우트에서는 조건부로 숨기거나PrivateRoute경계에 따라 렌더링을 제한하는 것을 검토해 주세요.Also applies to: 36-36
🤖 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/App.tsx` at line 4, App.tsx의 최상위에 전역 마운트된 FloatingButtonGroup이 /admin/* 관리자 라우트에서도 표시되지 않도록 제한하세요. AppRoutes 또는 PrivateRoute 경계를 기준으로 공개 라우트에서만 렌더링되게 조건부 처리하고, 관리자 페이지의 기존 라우팅 동작은 유지하세요.frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsx (1)
60-82: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win최대 길이를 초과하는 입력(붙여넣기 등)에 대한 처리 방식을 개선해 보세요.
현재
onChange핸들러는 입력된 값이 최대 길이를 초과하면 상태 업데이트를 완전히 무시합니다. 이로 인해 사용자가 긴 텍스트를 한 번에 붙여넣을 때 화면에 아무런 반응이 없어 불편을 겪을 수 있습니다.입력된 텍스트를 최대 길이에 맞춰 잘라내어 상태를 업데이트하도록
slice를 사용하는 것을 권장합니다.💡 제안하는 수정안 (텍스트 잘라내기)
const handleIntroChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - if (e.target.value.length <= INTRO_DESCRIPTION_MAX) { - setIntroDescription(e.target.value); - } + setIntroDescription(e.target.value.slice(0, INTRO_DESCRIPTION_MAX)); }; const handleActivityChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - if (e.target.value.length <= ACTIVITY_DESCRIPTION_MAX) { - setActivityDescription(e.target.value); - } + setActivityDescription(e.target.value.slice(0, ACTIVITY_DESCRIPTION_MAX)); }; const handleIdealChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - if (e.target.value.length <= IDEAL_CANDIDATE_MAX) { - setIdealCandidate({ ...idealCandidate, content: e.target.value }); - } + setIdealCandidate({ + ...idealCandidate, + content: e.target.value.slice(0, IDEAL_CANDIDATE_MAX), + }); }; const handleBenefitsChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - if (e.target.value.length <= BENEFITS_MAX) { - setBenefits(e.target.value); - } + setBenefits(e.target.value.slice(0, BENEFITS_MAX)); };🤖 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/ClubIntroEditTab/ClubIntroEditTabMobile.tsx` around lines 60 - 82, Update handleIntroChange, handleActivityChange, handleIdealChange, and handleBenefitsChange to always update their respective state using the input value truncated with slice(0, corresponding maximum). Preserve the existing idealCandidate object update while ensuring pasted or oversized text is visibly capped at its configured limit.
🤖 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/hooks/useShare.ts`:
- Around line 17-20: Wrap the requestShare call in the isRNWebView branch with
try/catch so synchronous bridge exceptions are handled, then continue to the
existing clipboard fallback when it throws or does not report success. Preserve
the current early return only when requestShare completes successfully.
In
`@frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/TextField/TextField.tsx`:
- Around line 29-31: In the TextField height-adjustment logic, update the
lineHeight parsing to fall back to a safe numeric default when
getComputedStyle(textarea).lineHeight parses as NaN, then use that validated
value for the two-line height calculation and CSS assignment.
---
Outside diff comments:
In
`@frontend/src/pages/AdminPage/components/MobileSaveButtonArea/MobileSaveButtonArea.styles.ts`:
- Around line 6-14: Restore an opaque white background on the SaveButtonArea
styled component so the fixed footer’s padding area does not reveal scrolling
content behind it.
---
Nitpick comments:
In `@frontend/src/App.tsx`:
- Line 4: App.tsx의 최상위에 전역 마운트된 FloatingButtonGroup이 /admin/* 관리자 라우트에서도 표시되지
않도록 제한하세요. AppRoutes 또는 PrivateRoute 경계를 기준으로 공개 라우트에서만 렌더링되게 조건부 처리하고, 관리자 페이지의
기존 라우팅 동작은 유지하세요.
In
`@frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsx`:
- Around line 60-82: Update handleIntroChange, handleActivityChange,
handleIdealChange, and handleBenefitsChange to always update their respective
state using the input value truncated with slice(0, corresponding maximum).
Preserve the existing idealCandidate object update while ensuring pasted or
oversized text is visibly capped at its configured limit.
🪄 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: ed928c0a-61cc-4b7b-ba08-1ab16e8f7357
⛔ Files ignored due to path filters (3)
frontend/src/assets/images/icons/scroll_icon.svgis excluded by!**/*.svgfrontend/src/assets/images/icons/scroll_to_top_icon.svgis excluded by!**/*.svgfrontend/src/assets/images/icons/share_floating_icon.svgis excluded by!**/*.svg
📒 Files selected for processing (53)
frontend/docs/features/admin/info/field-components.mdfrontend/docs/features/admin/info/mobile.mdfrontend/docs/features/admin/intro/mobile.mdfrontend/docs/features/components/header.mdfrontend/src/App.tsxfrontend/src/apis/application.tsfrontend/src/components/common/FloatingButtonGroup/FloatingButtonGroup.styles.tsfrontend/src/components/common/FloatingButtonGroup/FloatingButtonGroup.tsxfrontend/src/components/common/ScrollToTopButton/ScrollToTopButton.tsxfrontend/src/components/common/WebviewTopBar/WebviewTopBar.styles.tsfrontend/src/constants/CLAUDE.mdfrontend/src/constants/adminFieldLimits.tsfrontend/src/constants/adminFieldPlaceholders.tsfrontend/src/constants/initialFormData.tsfrontend/src/hooks/Scroll/useScrollTrigger.tsfrontend/src/hooks/useShare.tsfrontend/src/pages/AdminPage/components/MobileSaveButtonArea/MobileSaveButtonArea.styles.tsfrontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsxfrontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsxfrontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTabMobile.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/desktop/MakeTags/MakeTags.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/EditField/EditField.stories.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/EditField/EditField.styles.tsfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/EditField/EditField.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/FreeTagEditPage/FreeTagEditPage.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkField.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/NavField/NavField.stories.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/NavField/NavField.styles.tsfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/NavField/NavField.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/TextField/TextField.stories.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/TextField/TextField.styles.tsfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/TextField/TextField.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTab.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.styles.tsfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/desktop/FAQEditor/FAQEditor.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/AwardSection/AwardSection.stories.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/AwardSection/AwardSection.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/FAQSection/FAQSection.stories.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/FAQSection/FAQSection.styles.tsfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/FAQSection/FAQSection.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/InfoSection/InfoSection.stories.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/hooks/useClubIntroEdit.tsfrontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsxfrontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsxfrontend/src/types/application.tsfrontend/src/utils/CLAUDE.mdfrontend/src/utils/applicationFormGroup.tsfrontend/src/utils/semester.test.tsfrontend/src/utils/semester.ts
💤 Files with no reviewable changes (4)
- frontend/src/components/common/ScrollToTopButton/ScrollToTopButton.tsx
- frontend/src/utils/semester.test.ts
- frontend/src/utils/semester.ts
- frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/pages/AdminPage/components/MobileSaveButtonArea/MobileSaveButtonArea.styles.ts (1)
6-14: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win배경색 제거로 하단 고정 버튼 영역이 투명해질 수 있습니다.
SaveButtonArea는 화면 하단에 고정된 오버레이인데, 이전에 있던 흰색background선언이 제거되어 버튼을 감싸는 패딩 영역(상 10px, 좌우 20px)으로 스크롤 중인 페이지 콘텐츠가 비쳐 보일 수 있습니다. 의도된 변경이 아니라면 배경을 다시 추가해주세요.🎨 제안하는 수정
export const SaveButtonArea = styled.div` position: fixed; bottom: 0; left: 50%; transform: translateX(-50%); width: 100%; max-width: 500px; padding: 10px 20px calc(20px + env(safe-area-inset-bottom)); + background: ${colors.base.white}; z-index: ${Z_INDEX.clubDetailFooter};🤖 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/components/MobileSaveButtonArea/MobileSaveButtonArea.styles.ts` around lines 6 - 14, Restore an opaque white background on the SaveButtonArea styled component so the fixed footer’s padding area does not reveal scrolling content behind it.
🧹 Nitpick comments (2)
frontend/src/App.tsx (1)
4-4: 🎯 Functional Correctness | 🔵 Trivial
FloatingButtonGroup이 관리자 라우트에도 전역 노출됩니다.
AppRoutes바깥, 라우터 최상위에 마운트되어 있어/admin/*등 관리자 페이지에서도 공개용 공유/스크롤 버튼이 그대로 노출됩니다. PR 설명에서 이미 후속 논의 대상으로 언급된 사항이라 차단 이슈는 아니지만, 관리자 라우트에서는 조건부로 숨기거나PrivateRoute경계에 따라 렌더링을 제한하는 것을 검토해 주세요.Also applies to: 36-36
🤖 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/App.tsx` at line 4, App.tsx의 최상위에 전역 마운트된 FloatingButtonGroup이 /admin/* 관리자 라우트에서도 표시되지 않도록 제한하세요. AppRoutes 또는 PrivateRoute 경계를 기준으로 공개 라우트에서만 렌더링되게 조건부 처리하고, 관리자 페이지의 기존 라우팅 동작은 유지하세요.frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsx (1)
60-82: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win최대 길이를 초과하는 입력(붙여넣기 등)에 대한 처리 방식을 개선해 보세요.
현재
onChange핸들러는 입력된 값이 최대 길이를 초과하면 상태 업데이트를 완전히 무시합니다. 이로 인해 사용자가 긴 텍스트를 한 번에 붙여넣을 때 화면에 아무런 반응이 없어 불편을 겪을 수 있습니다.입력된 텍스트를 최대 길이에 맞춰 잘라내어 상태를 업데이트하도록
slice를 사용하는 것을 권장합니다.💡 제안하는 수정안 (텍스트 잘라내기)
const handleIntroChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - if (e.target.value.length <= INTRO_DESCRIPTION_MAX) { - setIntroDescription(e.target.value); - } + setIntroDescription(e.target.value.slice(0, INTRO_DESCRIPTION_MAX)); }; const handleActivityChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - if (e.target.value.length <= ACTIVITY_DESCRIPTION_MAX) { - setActivityDescription(e.target.value); - } + setActivityDescription(e.target.value.slice(0, ACTIVITY_DESCRIPTION_MAX)); }; const handleIdealChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - if (e.target.value.length <= IDEAL_CANDIDATE_MAX) { - setIdealCandidate({ ...idealCandidate, content: e.target.value }); - } + setIdealCandidate({ + ...idealCandidate, + content: e.target.value.slice(0, IDEAL_CANDIDATE_MAX), + }); }; const handleBenefitsChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - if (e.target.value.length <= BENEFITS_MAX) { - setBenefits(e.target.value); - } + setBenefits(e.target.value.slice(0, BENEFITS_MAX)); };🤖 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/ClubIntroEditTab/ClubIntroEditTabMobile.tsx` around lines 60 - 82, Update handleIntroChange, handleActivityChange, handleIdealChange, and handleBenefitsChange to always update their respective state using the input value truncated with slice(0, corresponding maximum). Preserve the existing idealCandidate object update while ensuring pasted or oversized text is visibly capped at its configured limit.
🤖 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/hooks/useShare.ts`:
- Around line 17-20: Wrap the requestShare call in the isRNWebView branch with
try/catch so synchronous bridge exceptions are handled, then continue to the
existing clipboard fallback when it throws or does not report success. Preserve
the current early return only when requestShare completes successfully.
In
`@frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/TextField/TextField.tsx`:
- Around line 29-31: In the TextField height-adjustment logic, update the
lineHeight parsing to fall back to a safe numeric default when
getComputedStyle(textarea).lineHeight parses as NaN, then use that validated
value for the two-line height calculation and CSS assignment.
---
Outside diff comments:
In
`@frontend/src/pages/AdminPage/components/MobileSaveButtonArea/MobileSaveButtonArea.styles.ts`:
- Around line 6-14: Restore an opaque white background on the SaveButtonArea
styled component so the fixed footer’s padding area does not reveal scrolling
content behind it.
---
Nitpick comments:
In `@frontend/src/App.tsx`:
- Line 4: App.tsx의 최상위에 전역 마운트된 FloatingButtonGroup이 /admin/* 관리자 라우트에서도 표시되지
않도록 제한하세요. AppRoutes 또는 PrivateRoute 경계를 기준으로 공개 라우트에서만 렌더링되게 조건부 처리하고, 관리자 페이지의
기존 라우팅 동작은 유지하세요.
In
`@frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsx`:
- Around line 60-82: Update handleIntroChange, handleActivityChange,
handleIdealChange, and handleBenefitsChange to always update their respective
state using the input value truncated with slice(0, corresponding maximum).
Preserve the existing idealCandidate object update while ensuring pasted or
oversized text is visibly capped at its configured limit.
🪄 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: ed928c0a-61cc-4b7b-ba08-1ab16e8f7357
⛔ Files ignored due to path filters (3)
frontend/src/assets/images/icons/scroll_icon.svgis excluded by!**/*.svgfrontend/src/assets/images/icons/scroll_to_top_icon.svgis excluded by!**/*.svgfrontend/src/assets/images/icons/share_floating_icon.svgis excluded by!**/*.svg
📒 Files selected for processing (53)
frontend/docs/features/admin/info/field-components.mdfrontend/docs/features/admin/info/mobile.mdfrontend/docs/features/admin/intro/mobile.mdfrontend/docs/features/components/header.mdfrontend/src/App.tsxfrontend/src/apis/application.tsfrontend/src/components/common/FloatingButtonGroup/FloatingButtonGroup.styles.tsfrontend/src/components/common/FloatingButtonGroup/FloatingButtonGroup.tsxfrontend/src/components/common/ScrollToTopButton/ScrollToTopButton.tsxfrontend/src/components/common/WebviewTopBar/WebviewTopBar.styles.tsfrontend/src/constants/CLAUDE.mdfrontend/src/constants/adminFieldLimits.tsfrontend/src/constants/adminFieldPlaceholders.tsfrontend/src/constants/initialFormData.tsfrontend/src/hooks/Scroll/useScrollTrigger.tsfrontend/src/hooks/useShare.tsfrontend/src/pages/AdminPage/components/MobileSaveButtonArea/MobileSaveButtonArea.styles.tsfrontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.tsxfrontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsxfrontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsxfrontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTabMobile.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/desktop/MakeTags/MakeTags.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/EditField/EditField.stories.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/EditField/EditField.styles.tsfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/EditField/EditField.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/FreeTagEditPage/FreeTagEditPage.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/LinkEditPage/LinkField.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/NavField/NavField.stories.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/NavField/NavField.styles.tsfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/NavField/NavField.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/TextField/TextField.stories.tsxfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/TextField/TextField.styles.tsfrontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/TextField/TextField.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTab.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.styles.tsfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/desktop/FAQEditor/FAQEditor.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/AwardSection/AwardSection.stories.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/AwardSection/AwardSection.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/FAQSection/FAQSection.stories.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/FAQSection/FAQSection.styles.tsfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/FAQSection/FAQSection.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/InfoSection/InfoSection.stories.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/hooks/useClubIntroEdit.tsfrontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsxfrontend/src/pages/ClubDetailPage/components/ShareButton/ShareButton.tsxfrontend/src/types/application.tsfrontend/src/utils/CLAUDE.mdfrontend/src/utils/applicationFormGroup.tsfrontend/src/utils/semester.test.tsfrontend/src/utils/semester.ts
💤 Files with no reviewable changes (4)
- frontend/src/components/common/ScrollToTopButton/ScrollToTopButton.tsx
- frontend/src/utils/semester.test.ts
- frontend/src/utils/semester.ts
- frontend/src/pages/AdminPage/tabs/ApplicationEditTab/ApplicationEditTab.tsx
🛑 Comments failed to post (2)
frontend/src/hooks/useShare.ts (1)
17-20: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
requestShare호출에 예외 처리가 없습니다.인앱 웹뷰 브릿지(
requestShare)가 동기적으로 예외를 던지면 catch되지 않아 unhandled promise rejection이 발생하고, 아래 clipboard fallback으로도 넘어가지 않습니다. 다른 두 경로(navigator.share, clipboard)는 모두 try/catch로 보호되어 있는 것과 대비됩니다.🔧 제안 수정
- if (isRNWebView) { - const isSent = requestShare({ title, text, url }); - if (isSent) return; - } + if (isRNWebView) { + try { + const isSent = requestShare({ title, text, url }); + if (isSent) return; + } catch { + // fall through to web fallback + } + }📝 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 (isRNWebView) { try { const isSent = requestShare({ title, text, url }); if (isSent) return; } catch { // fall through to web fallback } }🤖 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/hooks/useShare.ts` around lines 17 - 20, Wrap the requestShare call in the isRNWebView branch with try/catch so synchronous bridge exceptions are handled, then continue to the existing clipboard fallback when it throws or does not report success. Preserve the current early return only when requestShare completes successfully.frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/mobile/TextField/TextField.tsx (1)
29-31: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
lineHeight파싱 시NaN을 방지하기 위한 Fallback을 추가해 주세요.브라우저에 따라
getComputedStyle(textarea).lineHeight가"normal"을 반환할 수 있으며, 이 경우parseFloat은NaN을 반환합니다.NaN이 포함된 연산 결과가 CSS 속성값으로 들어가면 높이 자동 조절이 깨질 수 있으므로 안전한 기본값을 설정하는 것이 좋습니다.💻 제안하는 수정안
- const lineHeight = parseFloat(getComputedStyle(textarea).lineHeight); + const computedLineHeight = getComputedStyle(textarea).lineHeight; + const lineHeight = computedLineHeight === 'normal' ? 20 : parseFloat(computedLineHeight) || 20; textarea.style.height = 'auto'; textarea.style.height = `${Math.min(textarea.scrollHeight, lineHeight * 2)}px`;📝 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 computedLineHeight = getComputedStyle(textarea).lineHeight; const lineHeight = computedLineHeight === 'normal' ? 20 : parseFloat(computedLineHeight) || 20; textarea.style.height = 'auto'; textarea.style.height = `${Math.min(textarea.scrollHeight, lineHeight * 2)}px`;🤖 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/TextField/TextField.tsx` around lines 29 - 31, In the TextField height-adjustment logic, update the lineHeight parsing to fall back to a safe numeric default when getComputedStyle(textarea).lineHeight parses as NaN, then use that validated value for the two-line height calculation and CSS assignment.
- 위로 버튼: bounceUp keyframe (커진 상태로 위아래 무한 반복) - 공유 버튼: wiggle keyframe (커진 상태로 좌우 흔들림 1회) - 클릭 시 scale(1.08) 눌림 피드백 - $variant prop으로 버튼별 애니메이션 분기
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/components/common/FloatingButtonGroup/FloatingButtonGroup.stories.tsx`:
- Around line 16-31: Update both Styled.FloatingButton instances in the
FloatingButtonGroup story to provide the required $variant prop: use the scroll
variant for the scroll-to-top button and the share variant for the share button,
preserving their existing visibility and click behavior.
🪄 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: c5784f93-6bf0-48f0-8e2d-5d7932faec42
📒 Files selected for processing (3)
frontend/src/components/common/FloatingButtonGroup/FloatingButtonGroup.stories.tsxfrontend/src/components/common/FloatingButtonGroup/FloatingButtonGroup.styles.tsfrontend/src/components/common/FloatingButtonGroup/FloatingButtonGroup.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- frontend/src/components/common/FloatingButtonGroup/FloatingButtonGroup.tsx
| const handlePageShare = async () => { | ||
| const url = window.location.href; | ||
| await handleShare({ | ||
| title: document.title, | ||
| text: url, | ||
| url, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
트래킹 이벤트만 추가해주실 수 있을까요? 전역에서 쓰이기에 prop은 path가 나을 것 같아요
| import * as Styled from './FloatingButtonGroup.styles'; | ||
|
|
||
| export const FloatingButtonGroup = () => { | ||
| const { isTriggered: isScrolledDown, isScrollingUp } = useScrollTrigger(); |
There was a problem hiding this comment.
공유 버튼의 $isVisible가 true로 바뀌면서 isScrolledDown(isTriggered)이 더 이상 쓰이지 않게 됐어요. 사용하지 않는 값은 destructuring에서 빼 주시는 게 좋아요. 이대로면 useScrollTrigger가 반환하는 isTriggered 자체가 필요 없는지도 함께 확인해 주세요.
| const { isTriggered: isScrolledDown, isScrollingUp } = useScrollTrigger(); | |
| const { isScrollingUp } = useScrollTrigger(); |
| import * as Styled from './FloatingButtonGroup.styles'; | ||
|
|
||
| export const FloatingButtonGroup = () => { | ||
| const { isTriggered: isScrollingUp } = useScrollTrigger(); |
There was a problem hiding this comment.
isTriggered를 isScrollingUp으로 alias 했는데, 훅을 보면 isTriggered와 isScrollingUp은 서로 다른 값이에요. isTriggered는 (direction: 'down' 기준) scrollY > threshold, 즉 "일정 이상 스크롤 내려간 상태"를 뜻하고, isScrollingUp은 "실제로 위로 스크롤 중"을 뜻해요.
이번 변경으로 위로 이동 버튼이 "스크롤 올릴 때만" → "스크롤 내려가면 항상 노출"로 동작이 바뀐 건데, 만약 의도한 동작이라면 변수명이 실제 값(isTriggered)과 반대 의미라 헷갈려요. isScrolledDown처럼 실제 의미에 맞게 네이밍해 주는 게 좋아요.
| const { isTriggered: isScrollingUp } = useScrollTrigger(); | |
| const { isTriggered: isScrolledDown } = useScrollTrigger(); |
(이 경우 아래 $isVisible={isScrollingUp}도 isScrolledDown으로 함께 바꿔 주세요.) 혹시 원래대로 "위로 스크롤 시에만 노출"이 의도였다면 isScrollingUp 값을 그대로 구조분해해서 쓰는 게 맞아요.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
frontend/src/components/common/FloatingButtonGroup/FloatingButtonGroup.styles.ts (1)
43-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value모바일 기기에서 Hover 상태 고정(Sticky hover) 방지 권장
모바일 터치 기기에서는 버튼을 탭한 후
:hover상태가 계속 남아있어 아이콘이 커진 상태로 고정되는 어색한 사용성을 보일 수 있습니다. 이를 방지하기 위해 마우스(포인터)를 지원하는 기기에서만 hover 애니메이션이 동작하도록 미디어 쿼리를 추가하는 것을 제안합니다.💡 제안하는 수정안
- &:hover img { - transform: scale(1.12); - } + `@media` (hover: hover) and (pointer: fine) { + &:hover img { + transform: scale(1.12); + } + } &:active img { transform: scale(1.04); }🤖 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/components/common/FloatingButtonGroup/FloatingButtonGroup.styles.ts` around lines 43 - 49, Update the hover styling in FloatingButtonGroup.styles.ts so the &:hover img scale animation applies only on devices with a hover-capable mouse pointer, using an appropriate pointer/hover media query; keep the &:active img behavior unchanged.
🤖 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.
Nitpick comments:
In
`@frontend/src/components/common/FloatingButtonGroup/FloatingButtonGroup.styles.ts`:
- Around line 43-49: Update the hover styling in FloatingButtonGroup.styles.ts
so the &:hover img scale animation applies only on devices with a hover-capable
mouse pointer, using an appropriate pointer/hover media query; keep the &:active
img behavior unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d5405804-d12e-446b-a714-0dab7b8fcb8b
📒 Files selected for processing (3)
frontend/src/components/common/FloatingButtonGroup/FloatingButtonGroup.stories.tsxfrontend/src/components/common/FloatingButtonGroup/FloatingButtonGroup.styles.tsfrontend/src/components/common/FloatingButtonGroup/FloatingButtonGroup.tsx
💤 Files with no reviewable changes (1)
- frontend/src/components/common/FloatingButtonGroup/FloatingButtonGroup.stories.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- frontend/src/components/common/FloatingButtonGroup/FloatingButtonGroup.tsx
작업 내용
스크롤 위치에 따라 동적으로 표시되는 FloatingButtonGroup을 구현하고, 기존 ScrollToTopButton을 대체했습니다.
공유하기 기능을 useShare 훅으로 추출하여 FloatingButtonGroup에 통합했습니다.
주요 변경사항
ScrollToTopButton제거 →FloatingButtonGroup으로 대체useScrollTrigger에isScrollingUp감지 추가useShare훅으로 공유 로직 추출 (Kakao / Web Share API / 클립보드 복사 fallback)80px → 100px으로 조정하여 하단 고정 버튼과 자연스럽게 분리스크린샷
스토리북
👉스토리북
고민 사항
다음 작업 예정
동아리 상세페이지의 지원하기 버튼 영역에 현재 공유하기 버튼이 함께 표시되고 있습니다.
이를 제거하고, 관리자 페이지의 저장하기 버튼과 동일한
FixedBottomButton스타일로 통일할 예정입니다.ClubApplyButton내ShareButton제거FixedBottomButton공통 컴포넌트 기반으로 지원하기 버튼 통일Summary by CodeRabbit