[feature] 어드민 소개 정보 수정 모바일 공통 컴포넌트 구현#1784
Conversation
maxLength, currentLength, children을 optional → required로 변경 AwardSection 독립 후 불필요해진 NoCounter 스토리 제거
- 자체 Wrapper/Header/Label/Card 스타일로 독립 (Card padding 14px, min-height 52px) - Row → AwardRow, CountText → AchievementCount 네이밍 명확화 - NavButton을 styled.button → styled.span으로 변경 - getSemesterSortValue → getSortValue 간결화, isEmpty 변수 도입
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 44 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 (3)
Walkthroughtextarea 자동 높이 조절을 Changes모바일 섹션 및 공유 훅 추가
Sequence Diagram(s)sequenceDiagram
participant CustomTextArea
participant useAutoGrow
participant TextareaDOM
CustomTextArea->>useAutoGrow: useAutoGrow(disabled ? '' : value)
useAutoGrow->>TextareaDOM: height = 'auto'
useAutoGrow->>TextareaDOM: height = scrollHeight + 'px'
useAutoGrow-->>CustomTextArea: ref 반환
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 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 변경사항을 확인해주세요
1개 스토리 변경 · 전체 66개 스토리 · 26개 컴포넌트 |
There was a problem hiding this comment.
Code Review
This pull request introduces several new mobile components for the Admin Page, including AwardSection, FAQSection, and InfoSection, along with a custom useAutoGrow hook to handle auto-resizing text areas. Feedback on these changes highlights three key areas for improvement: ensuring useAutoGrow receives the actual value even when disabled to prevent text truncation during programmatic updates, utilizing the native maxLength attribute on the FAQ answer textarea for a smoother user experience, and replacing array indices with unique keys in the dynamic FAQ list to avoid React rendering and state bugs.
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.
| el.style.height = `${el.scrollHeight}px`; | ||
| } | ||
| }, [value]); | ||
| const textAreaRef = useAutoGrow(disabled ? '' : value); |
There was a problem hiding this comment.
disabled 상태일 때 useAutoGrow에 빈 문자열('')을 전달하도록 구현되어 있습니다. 이 경우, 컴포넌트가 비활성화(disabled={true})된 상태에서 부모 컴포넌트로부터 value가 프로그램적으로 업데이트되면, useAutoGrow 훅의 의존성 배열에 있는 값이 ''로 유지되므로 높이 조절 useEffect가 실행되지 않습니다. 이로 인해 텍스트가 변경되었음에도 높이가 늘어나지 않아 텍스트가 잘리는 현상이 발생할 수 있습니다.
비활성화 상태이더라도 텍스트 내용에 맞게 높이가 자동으로 조절되는 것이 올바른 UX이므로, disabled 여부와 관계없이 value를 직접 전달하는 것이 안전합니다.
| const textAreaRef = useAutoGrow(disabled ? '' : value); | |
| const textAreaRef = useAutoGrow(value); |
| <Styled.AnswerTextarea | ||
| ref={answerRef} | ||
| value={faq.answer} | ||
| onChange={(e) => { | ||
| if (e.target.value.length <= ANSWER_MAX_LENGTH) { | ||
| onChange(index, 'answer', e.target.value); | ||
| } | ||
| }} | ||
| placeholder='답변을 입력해주세요' | ||
| rows={1} | ||
| /> |
There was a problem hiding this comment.
AnswerTextarea에서 글자 수 제한을 구현하기 위해 onChange 이벤트 핸들러 내에서 직접 길이를 체크하고 있습니다. 이 방식은 사용자가 최대 글자 수를 초과하는 텍스트를 붙여넣거나 입력할 때 입력 자체가 완전히 무시되어 사용자 경험(UX) 측면에서 어색할 수 있습니다.
또한, QuestionInput 컴포넌트(34라인)처럼 HTML 표준 속성인 maxLength를 활용하면 브라우저가 기본적으로 글자 수 제한을 처리해 주므로 코드가 더 간결해지고 일관성을 유지할 수 있습니다.
| <Styled.AnswerTextarea | |
| ref={answerRef} | |
| value={faq.answer} | |
| onChange={(e) => { | |
| if (e.target.value.length <= ANSWER_MAX_LENGTH) { | |
| onChange(index, 'answer', e.target.value); | |
| } | |
| }} | |
| placeholder='답변을 입력해주세요' | |
| rows={1} | |
| /> | |
| <Styled.AnswerTextarea | |
| ref={answerRef} | |
| value={faq.answer} | |
| onChange={(e) => onChange(index, 'answer', e.target.value)} | |
| placeholder='답변을 입력해주세요' | |
| rows={1} | |
| maxLength={ANSWER_MAX_LENGTH} | |
| /> |
| {faqs.map((faq, index) => ( | ||
| <FAQItemEditor | ||
| key={index} | ||
| faq={faq} | ||
| index={index} | ||
| onChange={handleFieldChange} | ||
| onDelete={handleDelete} | ||
| /> | ||
| ))} |
There was a problem hiding this comment.
자주 묻는 질문(FAQ) 목록은 사용자가 동적으로 추가하거나 삭제할 수 있는 동적 리스트입니다. React에서 index를 key로 사용하면, 특정 항목을 삭제했을 때 React가 DOM 엘리먼트와 컴포넌트 상태(예: 포커스, 입력값, useAutoGrow 내부 상태 등)를 잘못 매핑하여 엉뚱한 컴포넌트가 업데이트되거나 포커스를 잃어버리는 버그가 발생할 수 있습니다.
따라서 각 FAQ 항목에 고유한 id를 부여하고 이를 key로 사용하는 것이 안전합니다. 새로운 FAQ를 추가할 때 crypto.randomUUID() 등을 이용해 고유 ID를 생성해 주거나, 데이터 구조상 ID를 다루기 어렵다면 로컬 상태에서 고유한 키를 관리하도록 개선하는 것을 권장합니다.
| {faqs.map((faq, index) => ( | |
| <FAQItemEditor | |
| key={index} | |
| faq={faq} | |
| index={index} | |
| onChange={handleFieldChange} | |
| onDelete={handleDelete} | |
| /> | |
| ))} | |
| {faqs.map((faq, index) => ( | |
| <FAQItemEditor | |
| key={faq.id || index} | |
| faq={faq} | |
| index={index} | |
| onChange={handleFieldChange} | |
| onDelete={handleDelete} | |
| /> | |
| ))} |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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/CustomTextArea/CustomTextArea.tsx`:
- Line 38: `CustomTextArea`의 `useAutoGrow`에 전달하는 값이 `disabled ? '' : value`라서
disabled 상태에서는 입력값 변경이 훅에 반영되지 않습니다. `CustomTextArea`와 `useAutoGrow` 호출부를 수정해
disabled 여부와 무관하게 value 변경 시 auto-grow가 다시 실행되도록 하세요. 필요하면 `useAutoGrow` 내부에서
disabled일 때 높이 조정만 건너뛰고 의존값은 실제 value를 유지하도록 조정하세요.
In
`@frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/AwardSection/AwardSection.tsx`:
- Around line 36-47: `AwardSection`의 `Styled.AwardRow`가 클릭만 가능한 div로 동작해 키보드
접근성이 빠져 있습니다. `onClick`을 사용하는 두 분기(없음 상태와 `sortedAwards.map`)에서
`Styled.AwardRow`를 `button` 기반 인터랙티브 요소로 바꾸거나, 최소한 `role="button"`, `tabIndex`,
`onKeyDown`을 추가해 Enter/Space로도 `onNavigate`가 실행되도록 수정해 주세요.
In
`@frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/FAQSection/FAQSection.styles.ts`:
- Around line 84-95: The focus styles are missing for keyboard accessibility in
FAQSection.styles.ts: QuestionInput removes outline without any replacement, and
the related button components also lack visible focus feedback. Update the
styled components for QuestionInput and the button blocks in the FAQ section to
add clear :focus-visible styling so keyboard users can see the active element,
while preserving the current visual design for mouse interactions.
🪄 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: e762a3ba-ace7-44bd-801e-29890ab8575d
⛔ Files ignored due to path filters (2)
frontend/src/assets/images/icons/add_icon.svgis excluded by!**/*.svgfrontend/src/assets/images/icons/close_circle_icon.svgis excluded by!**/*.svg
📒 Files selected for processing (11)
frontend/src/components/common/CustomTextArea/CustomTextArea.tsxfrontend/src/hooks/useAutoGrow.tsfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/AwardSection/AwardSection.stories.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/AwardSection/AwardSection.styles.tsfrontend/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/components/mobile/InfoSection/InfoSection.styles.tsfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/InfoSection/InfoSection.tsx
| el.style.height = `${el.scrollHeight}px`; | ||
| } | ||
| }, [value]); | ||
| const textAreaRef = useAutoGrow(disabled ? '' : value); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
비활성화 상태에서 값이 바뀌면 auto-grow가 다시 돌지 않습니다.
disabled ? '' : value 때문에 disabled 동안에는 훅 의존값이 항상 동일합니다. 그래서 API 응답처럼 value가 나중에 채워지는 경우에도 높이가 갱신되지 않아 긴 텍스트가 한 줄 높이에 갇힐 수 있습니다.
제안 수정
- const textAreaRef = useAutoGrow(disabled ? '' : value);
+ const textAreaRef = useAutoGrow(value);📝 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 textAreaRef = useAutoGrow(disabled ? '' : value); | |
| const textAreaRef = useAutoGrow(value); |
🤖 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/CustomTextArea/CustomTextArea.tsx` at line 38,
`CustomTextArea`의 `useAutoGrow`에 전달하는 값이 `disabled ? '' : value`라서 disabled
상태에서는 입력값 변경이 훅에 반영되지 않습니다. `CustomTextArea`와 `useAutoGrow` 호출부를 수정해 disabled
여부와 무관하게 value 변경 시 auto-grow가 다시 실행되도록 하세요. 필요하면 `useAutoGrow` 내부에서 disabled일 때
높이 조정만 건너뛰고 의존값은 실제 value를 유지하도록 조정하세요.
| <Styled.AwardRow onClick={() => onNavigate?.()}> | ||
| <Styled.EmptyText>없음</Styled.EmptyText> | ||
| <Styled.NavButton> | ||
| <img src={rightArrowIcon} alt='편집' /> | ||
| </Styled.NavButton> | ||
| </Styled.AwardRow> | ||
| ) : ( | ||
| sortedAwards.map((award) => ( | ||
| <Styled.AwardRow | ||
| key={`${award.year}-${award.semesterTerm}`} | ||
| onClick={() => onNavigate?.(award)} | ||
| > |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
클릭 가능한 div 대신 키보드 접근 가능한 인터랙티브 요소를 사용해 주세요.
Line 36, Line 44의 onClick이 div에 걸려 있어 키보드 탐색/활성화가 보장되지 않습니다. button(또는 role="button" + tabIndex + onKeyDown)으로 바꿔 접근성을 확보해 주세요.
🤖 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/components/mobile/AwardSection/AwardSection.tsx`
around lines 36 - 47, `AwardSection`의 `Styled.AwardRow`가 클릭만 가능한 div로 동작해 키보드
접근성이 빠져 있습니다. `onClick`을 사용하는 두 분기(없음 상태와 `sortedAwards.map`)에서
`Styled.AwardRow`를 `button` 기반 인터랙티브 요소로 바꾸거나, 최소한 `role="button"`, `tabIndex`,
`onKeyDown`을 추가해 Enter/Space로도 `onNavigate`가 실행되도록 수정해 주세요.
| export const QuestionInput = styled.input` | ||
| border: none; | ||
| outline: none; | ||
| background: transparent; | ||
| ${setTypography(typography.paragraph.p6)} | ||
| color: ${colors.base.black}; | ||
| width: 100%; | ||
|
|
||
| &::placeholder { | ||
| color: ${colors.gray[600]}; | ||
| } | ||
| `; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
포커스 표시가 제거되어 키보드 사용자 접근성이 깨집니다.
Line 85-86, Line 123-124에서 outline을 제거했지만 대체 :focus-visible 스타일이 없습니다. 버튼(Line 97-108, Line 144-162)도 동일하게 포커스 표시를 추가해 주세요.
Also applies to: 97-108, 122-136, 144-162
🤖 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/components/mobile/FAQSection/FAQSection.styles.ts`
around lines 84 - 95, The focus styles are missing for keyboard accessibility in
FAQSection.styles.ts: QuestionInput removes outline without any replacement, and
the related button components also lack visible focus feedback. Update the
styled components for QuestionInput and the button blocks in the FAQ section to
add clear :focus-visible styling so keyboard users can see the active element,
while preserving the current visual design for mouse interactions.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTab.tsx (1)
13-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
@/*alias import로 통일해주세요.여기서만 상대 경로를 쓰면 파일 이동 시 경로가 다시 깨지기 쉽고, 현재 프론트엔드 import 규칙과도 어긋납니다.
권장 수정안
-import AwardEditor from './components/desktop/AwardEditor/AwardEditor'; -import FAQEditor from './components/desktop/FAQEditor/FAQEditor'; +import AwardEditor from '`@/pages/AdminPage/tabs/ClubIntroEditTab/components/desktop/AwardEditor/AwardEditor`'; +import FAQEditor from '`@/pages/AdminPage/tabs/ClubIntroEditTab/components/desktop/FAQEditor/FAQEditor`';As per coding guidelines, "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/ClubIntroEditTab/ClubIntroEditTab.tsx` around lines 13 - 14, The imports in ClubIntroEditTab should use the `@/`* alias instead of relative paths, since the current two component imports break the frontend import convention and are fragile when files move. Update the AwardEditor and FAQEditor imports in ClubIntroEditTab to use the same `@/src-style` aliasing pattern used elsewhere in 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.
Nitpick comments:
In `@frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTab.tsx`:
- Around line 13-14: The imports in ClubIntroEditTab should use the `@/`* alias
instead of relative paths, since the current two component imports break the
frontend import convention and are fragile when files move. Update the
AwardEditor and FAQEditor imports in ClubIntroEditTab to use the same
`@/src-style` aliasing pattern used elsewhere in the codebase.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: db17774f-5c6c-4572-9ae4-99e346627ac3
📒 Files selected for processing (8)
frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTab.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/desktop/AwardEditor/AwardEditor.styles.tsfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/desktop/AwardEditor/AwardEditor.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/desktop/FAQEditor/FAQEditor.styles.tsfrontend/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/FAQSection/FAQSection.stories.tsxfrontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/InfoSection/InfoSection.stories.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/InfoSection/InfoSection.stories.tsx
- frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/AwardSection/AwardSection.stories.tsx
- frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/FAQSection/FAQSection.stories.tsx
| const useAutoGrow = (value: string) => { | ||
| const ref = useRef<HTMLTextAreaElement>(null); |
| onChange={(e) => { | ||
| if (e.target.value.length <= ANSWER_MAX_LENGTH) { | ||
| onChange(index, 'answer', e.target.value); | ||
| } |
There was a problem hiding this comment.
보통 저는 핸들러로 빼는 편인데 이 정도면 괜찮을까요? 어떻게 생각하시나요
There was a problem hiding this comment.
말씀 주신 것처럼 핸들러 분리가 더 깔끔할 것 같아서 handleAnswerChange로 분리했습니다 1f59902
| import { Award, SemesterTerm } from '@/types/club'; | ||
| import * as Styled from './AwardSection.styles'; | ||
|
|
||
| interface Props { |
|
Claude finished @suhyun113's task in 1m 3s —— View job 🤖 harry 리뷰 (incremental)이번 푸시(
변경 요약
총평이번 변경분은 모두 이전 리뷰 피드백(인터페이스명 명시화, 답변 onChange 핸들러 분리)을 반영한 깔끔한 리팩터링이에요. Props 인터페이스명을 컴포넌트별로 명시해 가독성이 좋아졌고,
|
#️⃣연관된 이슈
#1782
📝작업 내용
어드민 동아리 소개 편집 화면의 모바일 UI에서 사용하는 섹션 컴포넌트를 구현했습니다.
구현 컴포넌트
InfoSectionchildren으로 입력 내용을 유연하게 조합AwardSectionFAQSection컴포넌트 설계 결정
InfoSection을 공통으로, AwardSection·FAQSection을 별도 구현한 이유
처음에는
InfoSection을 공통 래퍼로 두고AwardSection,FAQSection모두 이를 상속하는 구조를 계획했습니다.구현 과정에서 각 섹션이 공통 구조에서 벗어나야 하는 이유가 생겼습니다.
FAQSection: FAQ 추가 버튼으로 카드를 동적으로 추가하는 구조라, 카드 하나로 children을 감싸는
InfoSection형태와 맞지 않아 별도 구현이 필요했습니다.AwardSection: 글자수 카운터가 없고, 카드 내부에 NavButton이 포함된 구조입니다. 이 필드 하나만을 위해
InfoSection에 optional prop을 추가하는 것보다 별도 구현이 더 적절하다고 판단했습니다.결과적으로
InfoSection은 글자수 카운터가 있는 텍스트 입력 섹션 전용 공통 컴포넌트로 확정했고,AwardSection·FAQSection은 각자Wrapper/Header/Label/Card스타일을 독립으로 가집니다.사용 방법
InfoSection — 글자수 제한이 있는 텍스트 입력 섹션
AwardSection — 수상 이력 표시 및 편집 진입
FAQSection — FAQ 목록 편집
Storybook
구현한 컴포넌트를 Storybook에서 확인할 수 있습니다.
👉 스토리북
경로: `Pages/AdminPage/tabs/ClubIntroEditTab/components/mobile
-> 이전에 구현한 ApplicationInfoEditTab의 경우 처음에는 AdminPage의 전체 공통 컴포넌트로 구현했으니, 각 페이지별 공통 컴포넌트로 구현하는 것이 맞다는 판단이었습니다. 이후 EditFields 컴포넌트는 ClubInfoEditTab으로 이동시킬 예정입니다. 또한 각 탭의 컴포넌트의 경우 모바일과 데스크탑의 사용이 많이 다르기 때문에 폴더로 분리했습니다.
🫡 참고사항
useAutoGrow훅을 별도 파일로 분리하여FAQSection의 답변 textarea 자동 높이 조절에 활용했습니다.Summary by CodeRabbit