-
Notifications
You must be signed in to change notification settings - Fork 0
[refactor] 마이페이지 관리자 진입 구조 정리 #281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "280-refactor-\uB9C8\uC774\uD398\uC774\uC9C0-\uAD00\uB9AC\uC790-\uC9C4\uC785-\uAD6C\uC870-\uC815\uB9AC"
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import type { ReactNode } from 'react'; | ||
| import ChatIcon from '@/assets/svg/chat.svg'; | ||
| import RightArrowIcon from '@/assets/svg/Chevron-left-dark.svg'; | ||
|
|
||
| type MyPageRowIcon = typeof ChatIcon; | ||
|
|
||
| interface MyPageRowBaseProps { | ||
| icon: MyPageRowIcon; | ||
| label: string; | ||
| } | ||
|
|
||
| interface MyPageRowLayoutProps extends MyPageRowBaseProps { | ||
| rightSlot: ReactNode; | ||
| labelClassName: string; | ||
| } | ||
|
|
||
| type MyPageLinkRowProps = MyPageRowBaseProps; | ||
|
|
||
| interface MyPageInfoRowProps extends MyPageRowBaseProps { | ||
| value: string; | ||
| } | ||
|
|
||
| type MyPageActionRowProps = MyPageRowBaseProps; | ||
|
|
||
| function MyPageRowLayout({ icon: Icon, label, rightSlot, labelClassName }: MyPageRowLayoutProps) { | ||
| return ( | ||
| <div className="flex items-center justify-between py-2"> | ||
| <div className="flex items-center gap-4"> | ||
| <Icon /> | ||
| <div className={labelClassName}>{label}</div> | ||
| </div> | ||
| {rightSlot} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export function MyPageLinkRow({ icon, label }: MyPageLinkRowProps) { | ||
| return <MyPageRowLayout icon={icon} label={label} rightSlot={<RightArrowIcon />} labelClassName="text-sub2" />; | ||
| } | ||
|
|
||
| export function MyPageInfoRow({ icon, label, value }: MyPageInfoRowProps) { | ||
| return ( | ||
| <MyPageRowLayout | ||
| icon={icon} | ||
| label={label} | ||
| rightSlot={<div className="text-[13px] leading-4 text-indigo-200">{value}</div>} | ||
| labelClassName="text-sm leading-4 font-semibold" | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| export function MyPageActionRow({ icon, label }: MyPageActionRowProps) { | ||
| return ( | ||
| <MyPageRowLayout icon={icon} label={label} rightSlot={null} labelClassName="text-sm leading-4 font-semibold" /> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,86 +1,109 @@ | ||
| import { useSuspenseQuery } from '@tanstack/react-query'; | ||
| import { Link } from 'react-router-dom'; | ||
| import { authQueries } from '@/apis/auth/queries'; | ||
| import { managedClubQueries } from '@/apis/club/managedQueries'; | ||
| import ChatIcon from '@/assets/svg/chat.svg'; | ||
| import RightArrowIcon from '@/assets/svg/chevron-right.svg'; | ||
| import RightArrowIcon from '@/assets/svg/Chevron-left-dark.svg'; | ||
| import FileSearchIcon from '@/assets/svg/file-search.svg'; | ||
| import FileIcon from '@/assets/svg/file.svg'; | ||
| import LayersIcon from '@/assets/svg/layers.svg'; | ||
| import LogoutIcon from '@/assets/svg/logout.svg'; | ||
| import UserIdCardIcon from '@/assets/svg/user-id-card.svg'; | ||
| import UserSquareIcon from '@/assets/svg/user-square.svg'; | ||
| import BottomModal from '@/components/common/BottomModal'; | ||
| import { MyPageActionRow, MyPageInfoRow, MyPageLinkRow } from '@/pages/User/MyPage/components/MyPageRows'; | ||
| import useBooleanState from '@/utils/hooks/useBooleanState'; | ||
| import { useAdminChatMutation } from '../hooks/useAdminChatMutation'; | ||
| import UserInfoCard from './components/UserInfoCard'; | ||
| import { useLogoutMutation } from './hooks/useLogout'; | ||
|
|
||
| const menuItems = [ | ||
| { to: 'manager', icon: UserIdCardIcon, label: '동아리 관리' }, | ||
| { to: '/legal/oss', icon: FileSearchIcon, label: '오픈소스 라이선스' }, | ||
| { to: '/legal/terms', icon: FileIcon, label: '코넥트 약관 확인' }, | ||
| { to: '/legal/privacy', icon: UserSquareIcon, label: '개인정보 처리 방침' }, | ||
| interface LegalMenuState { | ||
| backPath: string; | ||
| } | ||
|
|
||
| interface MenuItem { | ||
| to: string; | ||
| icon: typeof ChatIcon; | ||
| label: string; | ||
| state?: LegalMenuState; | ||
| } | ||
|
|
||
| interface ManagedClubSummary { | ||
| id: number; | ||
| name: string; | ||
| categoryName: string; | ||
| imageUrl: string; | ||
| } | ||
|
|
||
| interface ManagedClubLinkProps { | ||
| club: ManagedClubSummary; | ||
| } | ||
|
|
||
| const menuItems: MenuItem[] = [ | ||
| { to: '/legal/oss', icon: FileSearchIcon, label: '오픈소스 라이선스', state: { backPath: '/mypage' } }, | ||
| { to: '/legal/terms', icon: FileIcon, label: '코넥트 약관 확인', state: { backPath: '/mypage' } }, | ||
| { to: '/legal/privacy', icon: UserSquareIcon, label: '개인정보 처리 방침', state: { backPath: '/mypage' } }, | ||
| ]; | ||
|
|
||
| function ManagedClubLink({ club }: ManagedClubLinkProps) { | ||
| return ( | ||
| <Link to={`manager/${club.id}`} className="active:bg-indigo-5 flex items-center justify-between transition-colors"> | ||
| <div className="flex min-w-0 flex-1 items-center gap-3"> | ||
| <img | ||
| src={club.imageUrl} | ||
| alt="Club Avatar" | ||
| className="border-indigo-5 h-12 w-12 rounded-sm border object-cover" | ||
|
Comment on lines
+50
to
+53
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 대체 텍스트가 너무 일반적입니다. 지금은 모든 동아리 이미지가 스크린리더에서 동일하게 🤖 Prompt for AI Agents |
||
| /> | ||
| <div className="flex min-w-0 items-center gap-1.5"> | ||
| <span className="text-sub2 truncate text-indigo-700">{club.name}</span> | ||
| <span className="text-cap1 truncate text-indigo-300">{club.categoryName}</span> | ||
| </div> | ||
| </div> | ||
| <RightArrowIcon /> | ||
| </Link> | ||
| ); | ||
| } | ||
|
|
||
| function MyPage() { | ||
| const { data: myInfo } = useSuspenseQuery(authQueries.myInfo()); | ||
| const { data: managedClubList } = useSuspenseQuery(managedClubQueries.clubs()); | ||
| const { mutate: logout, isPending: isLoggingOut } = useLogoutMutation(); | ||
| const { value: isOpen, setTrue: openModal, setFalse: closeModal } = useBooleanState(false); | ||
| const { mutate: goToAdminChat, isPending: isCreatingAdminChat } = useAdminChatMutation(); | ||
|
|
||
| return ( | ||
| <div className="flex flex-col gap-2 p-3 pt-3"> | ||
| <div className="flex flex-col gap-5 p-3 pt-3"> | ||
| <UserInfoCard /> | ||
| <div className="flex flex-col gap-2 rounded-sm bg-white p-2"> | ||
| {menuItems | ||
| .filter(({ to }) => to !== 'manager' || myInfo.isClubManager || myInfo.role === 'ADMIN') | ||
| .map(({ to, icon: Icon, label }) => ( | ||
| <Link | ||
| key={to} | ||
| to={to} | ||
| state={to.startsWith('/legal/') ? { backPath: '/mypage' } : undefined} | ||
| className="bg-indigo-0 active:bg-indigo-5 rounded-sm transition-colors" | ||
| > | ||
| <div className="flex items-center justify-between px-3 py-2"> | ||
| <div className="flex items-center gap-4"> | ||
| <Icon /> | ||
| <div className="text-sub2">{label}</div> | ||
| </div> | ||
| <RightArrowIcon /> | ||
| </div> | ||
| </Link> | ||
|
|
||
| <section className="flex flex-col gap-2"> | ||
| <span className="text-text-700 leading-4.5 font-semibold">관리중인 동아리</span> | ||
| <div className="flex flex-col gap-3 rounded-2xl bg-white p-3"> | ||
| {managedClubList.joinedClubs.map((club) => ( | ||
| <ManagedClubLink key={club.id} club={club} /> | ||
| ))} | ||
| </div> | ||
| </section> | ||
| <div className="flex flex-col gap-2 rounded-2xl bg-white px-3 py-2"> | ||
| {menuItems.map(({ to, icon, label, state }) => ( | ||
| <Link key={to} to={to} state={state} className="bg-indigo-0 active:bg-indigo-5 rounded-sm transition-colors"> | ||
| <MyPageLinkRow icon={icon} label={label} /> | ||
| </Link> | ||
| ))} | ||
|
|
||
| <button | ||
| disabled={isCreatingAdminChat} | ||
| onClick={() => goToAdminChat()} | ||
| className="bg-indigo-0 active:bg-indigo-5 w-full rounded-sm text-left transition-colors disabled:cursor-not-allowed disabled:opacity-50" | ||
| > | ||
| <div className="flex items-center justify-between px-3 py-2"> | ||
| <div className="flex items-center gap-4"> | ||
| <ChatIcon /> | ||
| <div className="text-sub2">{isCreatingAdminChat ? '이동 중...' : '문의하기'}</div> | ||
| </div> | ||
| <RightArrowIcon /> | ||
| </div> | ||
| <MyPageLinkRow icon={ChatIcon} label={isCreatingAdminChat ? '이동 중...' : '문의하기'} /> | ||
| </button> | ||
|
|
||
| <div className="bg-indigo-0 rounded-sm"> | ||
| <div className="flex items-center justify-between px-3 py-2"> | ||
| <div className="flex items-center gap-4"> | ||
| <LayersIcon /> | ||
| <div className="text-sm leading-4 font-semibold">버전관리</div> | ||
| </div> | ||
| <div className="text-[13px] leading-4 text-indigo-200"> | ||
| {window.APP_VERSION ? `v${window.APP_VERSION}` : '-'} | ||
| </div> | ||
| </div> | ||
| <MyPageInfoRow | ||
| icon={LayersIcon} | ||
| label="버전관리" | ||
| value={window.APP_VERSION ? `v${window.APP_VERSION}` : '-'} | ||
| /> | ||
| </div> | ||
| <button className="bg-indigo-0 flex items-center rounded-sm px-3 py-2" onClick={openModal}> | ||
| <div className="flex items-center gap-4"> | ||
| <LogoutIcon /> | ||
| <div className="text-sm leading-4 font-semibold">로그아웃</div> | ||
| </div> | ||
| <button className="bg-indigo-0 rounded-sm" onClick={openModal}> | ||
| <MyPageActionRow icon={LogoutIcon} label="로그아웃" /> | ||
| </button> | ||
| </div> | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
클릭 가능한
Card에 키보드 접근성 보완이 필요합니다.현재
onClick만 사용하면 키보드 사용자에게는 진입이 막힐 수 있습니다.button/Link로 감싸거나role="button",tabIndex,onKeyDown을 함께 처리해주세요.간단한 보완 예시
As per coding guidelines, "동적 className 조합, 접근성(aria-*, role, 키보드 탐색), Props 타입 일관성을 우선 확인하는지".
📝 Committable suggestion
🤖 Prompt for AI Agents