Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ const ManagedApplicationDetail = lazy(() => import('./pages/Manager/ManagedAppli
const ManagedApplicationList = lazy(() => import('./pages/Manager/ManagedApplicationList'));
const ManagedClubDetail = lazy(() => import('./pages/Manager/ManagedClubDetail'));
const ManagedClubInfo = lazy(() => import('./pages/Manager/ManagedClubProfile'));
const ManagedClubList = lazy(() => import('./pages/Manager/ManagedClubList'));
const ManagedMemberApplicationDetail = lazy(() => import('./pages/Manager/ManagedMemberApplicationDetail'));
const ManagedMemberList = lazy(() => import('./pages/Manager/ManagedMemberList'));
const ManagedRecruitment = lazy(() => import('./pages/Manager/ManagedRecruitment'));
Expand Down Expand Up @@ -94,7 +93,6 @@ function App() {
<Route path="mypage">
<Route index element={<MyPage />} />
<Route path="manager">
<Route index element={<ManagedClubList />} />
<Route path=":clubId" element={<ManagedClubDetail />} />
<Route path=":clubId/members" element={<ManagedMemberList />} />
<Route path=":clubId/members/:userId/application" element={<ManagedMemberApplicationDetail />} />
Expand Down
10 changes: 5 additions & 5 deletions src/components/common/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { HTMLAttributes, ReactNode } from 'react';
import clsx from 'clsx';
import { twMerge } from 'tailwind-merge';
import { cn } from '@/utils/ts/cn';

interface CardProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode;
}

function Card({ children, className, ...props }: CardProps) {
const base = 'border-indigo-5 flex w-full flex-col gap-3 rounded-lg border bg-white p-3';

return (
<div className={twMerge(clsx(base, className))} {...props}>
<div
className={cn('border-indigo-5 flex w-full flex-col gap-3 rounded-lg border bg-white p-3', className)}
{...props}
>
{children}
</div>
);
Expand Down
15 changes: 0 additions & 15 deletions src/components/layout/Header/components/ProfileHeader.tsx

This file was deleted.

7 changes: 2 additions & 5 deletions src/components/layout/Header/headerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,10 @@ export const HEADER_CONFIGS: HeaderConfig[] = [
type: 'default',
match: (pathname) => /^\/clubs\/\d+$/.test(pathname),
},
{
type: 'profile',
match: (pathname) => pathname === '/mypage',
},
{
type: 'info',
match: (pathname) => pathname === '/home' || pathname === '/timer' || pathname === '/council',
match: (pathname) =>
pathname === '/home' || pathname === '/timer' || pathname === '/council' || pathname === '/mypage',
},
{
type: 'chatList',
Expand Down
2 changes: 0 additions & 2 deletions src/components/layout/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import DefaultHeader from './components/DefaultHeader';
import InfoHeader from './components/InfoHeader';
import ManagerHeader from './components/ManagerHeader';
import PlainSubpageHeader from './components/PlainSubpageHeader';
import ProfileHeader from './components/ProfileHeader';
import ScheduleHeader from './components/ScheduleHeader';
import SubpageHeader from './components/SubpageHeader';
import { getHeaderPresentation } from './presentation';
Expand All @@ -22,7 +21,6 @@ function Header({ headerRef }: HeaderProps) {
const { title, type: headerType } = getHeaderPresentation(pathname);

const HEADER_RENDERERS: Record<HeaderType, HeaderRenderer> = {
profile: ({ headerRef }) => <ProfileHeader headerRef={headerRef} />,
info: ({ headerRef }) => <InfoHeader headerRef={headerRef} />,
chatList: ({ title, headerRef }) => <ChatListHeader title={title} headerRef={headerRef} />,
chat: ({ headerRef }) => <ChatHeader headerRef={headerRef} />,
Expand Down
1 change: 0 additions & 1 deletion src/components/layout/Header/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { ReactNode, Ref } from 'react';

export type HeaderType =
| 'info'
| 'profile'
| 'chat'
| 'none'
| 'notification'
Expand Down
42 changes: 0 additions & 42 deletions src/pages/Manager/ManagedClubList/index.tsx

This file was deleted.

56 changes: 56 additions & 0 deletions src/pages/User/MyPage/components/MyPageRows.tsx
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" />
);
}
2 changes: 1 addition & 1 deletion src/pages/User/MyPage/components/UserInfoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function UserInfoCard() {
};

return (
<Card className="active:bg-indigo-5/50 cursor-pointer gap-0 p-3" onClick={handleCardClick}>
<Card className="active:bg-indigo-5/50 cursor-pointer gap-0 rounded-2xl p-3" onClick={handleCardClick}>
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.

⚠️ Potential issue | 🟠 Major

클릭 가능한 Card에 키보드 접근성 보완이 필요합니다.

현재 onClick만 사용하면 키보드 사용자에게는 진입이 막힐 수 있습니다. button/Link로 감싸거나 role="button", tabIndex, onKeyDown을 함께 처리해주세요.

간단한 보완 예시
-    <Card className="active:bg-indigo-5/50 cursor-pointer gap-0 rounded-2xl p-3" onClick={handleCardClick}>
+    <Card
+      className="active:bg-indigo-5/50 cursor-pointer gap-0 rounded-2xl p-3"
+      role="button"
+      tabIndex={0}
+      onClick={handleCardClick}
+      onKeyDown={(e) => {
+        if (e.key === 'Enter' || e.key === ' ') handleCardClick();
+      }}
+    >

As per coding guidelines, "동적 className 조합, 접근성(aria-*, role, 키보드 탐색), Props 타입 일관성을 우선 확인하는지".

📝 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
<Card className="active:bg-indigo-5/50 cursor-pointer gap-0 rounded-2xl p-3" onClick={handleCardClick}>
<Card
className="active:bg-indigo-5/50 cursor-pointer gap-0 rounded-2xl p-3"
role="button"
tabIndex={0}
onClick={handleCardClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') handleCardClick();
}}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/User/MyPage/components/UserInfoCard.tsx` at line 36, The clickable
Card using the Card component with onClick={handleCardClick} lacks keyboard
accessibility; update the Card (or wrap it) to be keyboard-focusable by either
rendering it as a semantic interactive element (button/Link) or by adding
role="button", tabIndex={0}, and an onKeyDown handler that invokes
handleCardClick when Enter or Space is pressed; ensure the same props
(aria-label if needed) and any dynamic className logic remain unchanged and that
handleCardClick is idempotent for both click and key activation.

<div className="flex items-center gap-3">
<UserAvatar imageUrl={myInfo.imageUrl} name={myInfo.name} />
<div className="min-w-0">
Expand Down
121 changes: 72 additions & 49 deletions src/pages/User/MyPage/index.tsx
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
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.

⚠️ Potential issue | 🟡 Minor

대체 텍스트가 너무 일반적입니다.

지금은 모든 동아리 이미지가 스크린리더에서 동일하게 "Club Avatar"로 읽힙니다. 장식용이면 alt=""로 비우고, 정보용이면 club.name 기반으로 넣어 항목별로 구분되게 해주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/User/MyPage/index.tsx` around lines 50 - 53, The <img> using
club.imageUrl currently has a generic alt "Club Avatar"; change the alt handling
in the JSX so decorative images use alt="" and informative images use the club's
name (e.g., alt={club.name}) so each club is distinguishable by screen readers;
update the <img> element where club.imageUrl is used (reference the img tag and
club.name) to set alt conditionally or replace the static string with club.name
as appropriate.

/>
<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>

Expand Down
6 changes: 4 additions & 2 deletions src/utils/hooks/useSmartBack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ export function useSmartBack() {
const parts = pathname.split('/');
const clubId = parts[3];

if (parts[4] === 'info') {
if (parts.length === 4) {
targetPath = '/mypage';
} else if (parts[4] === 'info') {
targetPath = `/mypage/manager/${clubId}`;
} else if (parts[4] === 'recruitment' && parts[5]) {
targetPath = `/mypage/manager/${clubId}/recruitment`;
Expand All @@ -76,7 +78,7 @@ export function useSmartBack() {
} else if (parts[4] === 'members') {
targetPath = `/mypage/manager/${clubId}`;
} else {
targetPath = `/mypage/manager`;
targetPath = '/mypage';
}
} else if (pathname === '/mypage/manager') {
targetPath = '/mypage';
Expand Down
Loading