diff --git a/frontend/src/components/common/InputField/InputField.styles.ts b/frontend/src/components/common/InputField/InputField.styles.ts index 3ab608b8b..e7f8afe7f 100644 --- a/frontend/src/components/common/InputField/InputField.styles.ts +++ b/frontend/src/components/common/InputField/InputField.styles.ts @@ -13,7 +13,7 @@ export const InputContainer = styled.div<{ width: string; readOnly?: boolean }>` export const Label = styled.label` font-size: 1.125rem; - margin-bottom: 12px; + margin-bottom: 8px; font-weight: 600; `; @@ -23,13 +23,17 @@ export const InputWrapper = styled.div` align-items: center; `; -export const Input = styled.input<{ hasError?: boolean; readOnly?: boolean; isSuccess?: boolean; }>` +export const Input = styled.input<{ + hasError?: boolean; + readOnly?: boolean; + isSuccess?: boolean; +}>` flex: 1; height: 45px; padding: 12px 18px; border: 1px solid ${({ hasError, isSuccess }) => - hasError ? 'red' : isSuccess ? '#28a745' : '#c5c5c5'}; + hasError ? 'red' : isSuccess ? '#28a745' : '#c5c5c5'}; background-color: transparent; border-radius: 6px; outline: none; @@ -50,10 +54,10 @@ export const Input = styled.input<{ hasError?: boolean; readOnly?: boolean; isSu readOnly ? '#c5c5c5' : hasError - ? 'red' - : isSuccess - ? '#28a745' - : '#007bff'}; + ? 'red' + : isSuccess + ? '#28a745' + : '#007bff'}; ${({ readOnly }) => readOnly && 'cursor: pointer;'} } @@ -65,7 +69,7 @@ export const Input = styled.input<{ hasError?: boolean; readOnly?: boolean; isSu color: rgba(0, 0, 0, 0.3); transition: color 0.25s ease; } - + &:focus::placeholder { color: transparent; } @@ -125,4 +129,3 @@ export const HelperText = styled.div` white-space: nowrap; z-index: 1; `; - diff --git a/frontend/src/pages/AdminPage/AdminPage.styles.ts b/frontend/src/pages/AdminPage/AdminPage.styles.ts index 3fe8954d2..bbdae9e93 100644 --- a/frontend/src/pages/AdminPage/AdminPage.styles.ts +++ b/frontend/src/pages/AdminPage/AdminPage.styles.ts @@ -1,24 +1,23 @@ import styled from 'styled-components'; - -export const AdminPageContainer = styled.div` - display: flex; - margin-top: 98px; - align-items: flex-start; +export const Background = styled.div` + background-color: #f2f2f2; + min-height: 100vh; `; -export const Divider = styled.div` - position: sticky; - top: 98px; - width: 1px; - height: calc(100vh - 98px); - background-color: #dcdcdc; - margin: 0 34px; - flex-shrink: 0; +export const Layout = styled.div` + display: flex; + gap: 30px; + max-width: 1180px; + margin: 0 auto; + width: 100%; + padding-top: 110px; `; - -export const Content = styled.main` +export const MainContent = styled.main` width: 100%; - max-width: 977px; - padding: 62px 58px; + max-width: 960px; + background-color: #ffffff; + padding: 54px; + border-radius: 20px; + margin-bottom: 50px; `; diff --git a/frontend/src/pages/AdminPage/AdminPage.tsx b/frontend/src/pages/AdminPage/AdminPage.tsx index b3c3b0291..8534f9ae1 100644 --- a/frontend/src/pages/AdminPage/AdminPage.tsx +++ b/frontend/src/pages/AdminPage/AdminPage.tsx @@ -1,11 +1,10 @@ import Header from '@/components/common/Header/Header'; -import { PageContainer } from '@/styles/PageContainer.styles'; import SideBar from '@/pages/AdminPage/components/SideBar/SideBar'; import { Outlet } from 'react-router-dom'; -import * as Styled from './AdminPage.styles'; + import { useGetClubDetail } from '@/hooks/queries/club/useGetClubDetail'; import { useAdminClubContext } from '@/context/AdminClubContext'; -import { Divider } from './components/SideBar/SideBar.styles'; +import * as Styled from './AdminPage.styles'; const AdminPage = () => { const { clubId } = useAdminClubContext(); @@ -20,18 +19,14 @@ const AdminPage = () => { return ( <>
- - - - - + + + + - - - + + + ); }; diff --git a/frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.styles.ts b/frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.styles.ts index 0efa1df1f..fa745f2b6 100644 --- a/frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.styles.ts +++ b/frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.styles.ts @@ -1,81 +1,83 @@ import styled from 'styled-components'; +export const Label = styled.label` + display: block; + padding: 0 4px; + font-size: 1.125rem; + font-weight: 600; + margin-bottom: 4px; +`; + +export const ContentWrapper = styled.div` + display: flex; + align-items: center; + gap: 16px; +`; + export const ClubLogoWrapper = styled.div` position: relative; - width: 168px; - height: 168px; + width: 100px; + height: 100px; `; export const ClubLogo = styled.img` - width: 168px; - height: 168px; + width: 100px; + height: 100px; background: #ededed; - border-radius: 10px; + border-radius: 20px; object-fit: cover; `; -export const EditButton = styled.button` - position: absolute; - bottom: 8px; - right: 8px; - width: 32px; - height: 32px; - background: transparent; - border: none; - outline: none; - border-radius: 50%; +export const ButtonTextGroup = styled.div` display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - - img { - width: 24px; - height: 24px; - } + flex-direction: column; + gap: 6px; `; -export const EditMenu = styled.div` - position: absolute; - left: 100%; - transform: translateY(-50%); - margin-left: 8px; - background: #fff; - box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.4); - border-radius: 18px; - overflow: hidden; - min-width: 160px; - z-index: 2; +export const ButtonGroup = styled.div` + display: flex; + gap: 6px; `; -export const EditMenuItem = styled.button` - width: 100%; - display: flex; - align-items: center; - gap: 10px; - padding: 12px 16px; +export const UploadButton = styled.button` + padding: 10px 20px; + border: 1px solid #ff6b35; + border-radius: 80px; background: white; - border: none; - font-size: 16px; - color: #333; + color: #ff6b35; + font-size: 12px; + line-height: 140%; + font-weight: 600; cursor: pointer; - transition: background-color 0.2s; + transition: all 0.2s; &:hover { - background-color: #f1f1f1; + background: #ff6b35; + color: white; } +`; - img { - width: 15px; - height: 15px; +export const ResetButton = styled.button` + padding: 10px 20px; + border: 1px solid #999; + border-radius: 80px; + background: white; + color: #999; + font-size: 12px; + line-height: 140%; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: #999; + color: white; } `; -export const Divider = styled.div` - width: 90%; - height: 1px; - background: rgba(0, 0, 0, 0.12); - margin: 0 auto; +export const HelpText = styled.p` + font-size: 12px; + color: #c5c5c5; `; export const HiddenFileInput = styled.input` diff --git a/frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx b/frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx index b17361264..c8dca7e87 100644 --- a/frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx +++ b/frontend/src/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor.tsx @@ -1,10 +1,6 @@ -import React, { useState, useRef, useCallback, useEffect } from 'react'; +import React, { useRef } from 'react'; import * as Styled from './ClubLogoEditor.styles'; import defaultLogo from '@/assets/images/logos/default_profile_image.svg'; -import changeImageIcon from '@/assets/images/icons/change_image_button_icon.svg'; -import changeImageIconHover from '@/assets/images/icons/change_image_button_icon_hover.svg'; -import editIcon from '@/assets/images/icons/pencil_icon_2.svg'; -import deleteIcon from '@/assets/images/icons/cancel_button_icon.svg'; import { useUploadLogo, useDeleteLogo, @@ -24,8 +20,6 @@ const ClubLogoEditor = ({ clubLogo }: ClubLogoEditorProps) => { const { clubId } = useAdminClubContext(); if (!clubId) return null; - const [isMenuOpen, setIsMenuOpen] = useState(false); - const menuRef = useRef(null); const fileInputRef = useRef(null); const uploadMutation = useUploadLogo(); @@ -34,10 +28,6 @@ const ClubLogoEditor = ({ clubLogo }: ClubLogoEditorProps) => { const isClubLogoEmpty = !clubLogo || clubLogo.trim() === ''; const displayedLogo = isClubLogoEmpty ? defaultLogo : clubLogo; - const toggleMenu = useCallback(() => { - setIsMenuOpen((prev) => !prev); - }, [trackEvent]); - const handleFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -50,6 +40,7 @@ const ClubLogoEditor = ({ clubLogo }: ClubLogoEditorProps) => { return; } + trackEvent(ADMIN_EVENT.CLUB_LOGO_UPLOAD_BUTTON_CLICKED); uploadMutation.mutate({ clubId, file }); }; @@ -66,71 +57,42 @@ const ClubLogoEditor = ({ clubLogo }: ClubLogoEditorProps) => { if (!window.confirm('정말 로고를 기본 이미지로 되돌릴까요?')) return; + trackEvent(ADMIN_EVENT.CLUB_LOGO_RESET_BUTTON_CLICKED); deleteMutation.mutate(clubId); }; - useEffect(() => { - if (!isMenuOpen) return; - - const handleOutsideClick = (e: MouseEvent) => { - if (menuRef.current && !menuRef.current.contains(e.target as Node)) { - setIsMenuOpen(false); - } - }; - - document.addEventListener('mousedown', handleOutsideClick); - - return () => { - document.removeEventListener('mousedown', handleOutsideClick); - }; - }, [isMenuOpen]); - return ( - - - - - 로고 수정 + 로고 + + + + + + + + + 이미지 수정 + + + {!isClubLogoEmpty && ( + + 초기화 + + )} + + + 동아리 로고 이미지를 넣어주세요. + + + - - - {isMenuOpen && ( - - { - trackEvent(ADMIN_EVENT.CLUB_LOGO_EDIT_BUTTON_CLICKED); - triggerFileInput(); - setIsMenuOpen(false); - }} - > - 사진 수정 아이콘 - 사진 수정하기 - - - - - { - trackEvent(ADMIN_EVENT.CLUB_LOGO_RESET_BUTTON_CLICKED); - handleLogoReset(); - setIsMenuOpen(false); - }} - > - 초기화 아이콘 - 초기화하기 - - - )} - - - + + ); }; diff --git a/frontend/src/pages/AdminPage/components/ContentSection/ContentSection.styles.ts b/frontend/src/pages/AdminPage/components/ContentSection/ContentSection.styles.ts new file mode 100644 index 000000000..7f686575c --- /dev/null +++ b/frontend/src/pages/AdminPage/components/ContentSection/ContentSection.styles.ts @@ -0,0 +1,28 @@ +import styled from 'styled-components'; + +export const ContentSection = styled.section` + display: flex; + flex-direction: column; + gap: 30px; +`; + +export const ContentSectionHeader = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + min-height: 42px; +`; + +export const ContentSectionTitle = styled.h2` + font-size: 1.5rem; + font-weight: bold; + letter-spacing: 0; + margin: 0; +`; + +export const ContentSectionBody = styled.div` + display: flex; + flex-direction: column; + gap: 16px; +`; diff --git a/frontend/src/pages/AdminPage/components/ContentSection/ContentSection.tsx b/frontend/src/pages/AdminPage/components/ContentSection/ContentSection.tsx new file mode 100644 index 000000000..56807b8c1 --- /dev/null +++ b/frontend/src/pages/AdminPage/components/ContentSection/ContentSection.tsx @@ -0,0 +1,40 @@ +import { ReactNode } from 'react'; +import * as Styled from './ContentSection.styles'; + +interface ContentSectionProps { + children: ReactNode; +} + +interface ContentSectionHeaderProps { + title: string; + action?: ReactNode; +} + +interface ContentSectionBodyProps { + children: ReactNode; +} + +const ContentSectionRoot = ({ children }: ContentSectionProps) => { + return {children}; +}; + +const ContentSectionHeader = ({ + title, + action, +}: ContentSectionHeaderProps) => { + return ( + + {title} + {action &&
{action}
} +
+ ); +}; + +const ContentSectionBody = ({ children }: ContentSectionBodyProps) => { + return {children}; +}; + +export const ContentSection = Object.assign(ContentSectionRoot, { + Header: ContentSectionHeader, + Body: ContentSectionBody, +}); diff --git a/frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts b/frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts index 472c4e056..48fc40315 100644 --- a/frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts +++ b/frontend/src/pages/AdminPage/components/SideBar/SideBar.styles.ts @@ -3,60 +3,39 @@ import styled from 'styled-components'; export const SidebarWrapper = styled.aside` display: flex; flex-direction: column; - align-items: left; - word-wrap: break-word; - overflow-wrap: break-word; - white-space: normal; - width: 168px; position: sticky; - top: 98px; + top: 110px; + width: 190px; + padding: 12px 10px 10px; + border-radius: 20px; + background-color: #ffffff; height: fit-content; `; export const SidebarHeader = styled.p` - font-size: 1.5rem; + padding: 0 8px; + font-size: 1.25rem; font-weight: bold; - letter-spacing: 0; - margin-left: 11px; - margin-bottom: 19px; `; -export const ClubLogo = styled.img` - width: 168px; - height: 168px; - background: #ededed; - border-radius: 10px; -`; - -export const ClubTitle = styled.p` - text-align: center; - margin-top: 14px; - font-size: 2rem; - font-weight: bold; - height: 76px; - max-width: 163px; -`; - -export const Divider = styled.hr` +export const SidebarDivider = styled.hr` width: 100%; - border: 1px solid; - color: #C5C5C5; - height: 0; - margin: 16px 0px 16px 0px; + margin: 10px 0 12px; + border: none; + border-top: 1px solid #c5c5c5; `; export const SidebarButtonContainer = styled.ul` display: flex; flex-direction: column; - gap: 16px; + gap: 12px; list-style: none; `; export const SidebarCategoryTitle = styled.p` - align-items: center; - padding: 6px 0px 6px 10px; + padding: 6px 10px; font-size: 0.75rem; - font-weight: medium; + font-weight: 500; color: #989898; `; @@ -65,23 +44,17 @@ export const SidebarButton = styled.button` box-sizing: border-box; display: flex; align-items: center; - cursor: pointer; - width: 100%; - height: 37px; + padding: 8px 10px; border-radius: 10px; - - padding-left: 10px; - font-size: 1rem; - font-weight: medium; - + font-weight: 500; + cursor: pointer; transition: background-color 0.1s ease; &.active { background-color: rgba(255, 117, 67, 1); color: white; - font-weight: medium; + font-weight: 600; } `; - diff --git a/frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx b/frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx index 27390fa4d..8796ed13d 100644 --- a/frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx +++ b/frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx @@ -1,16 +1,10 @@ -import React, { useMemo } from 'react'; +import { useMemo } from 'react'; import * as Styled from './SideBar.styles'; import { useNavigate, useLocation } from 'react-router-dom'; -import ClubLogoEditor from '@/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor'; import { logout } from '@/apis/auth/logout'; import useMixpanelTrack from '@/hooks/useMixpanelTrack'; import { ADMIN_EVENT } from '@/constants/eventName'; -interface SideBarProps { - clubName: string; - clubLogo: string; -} - interface TabItem { label: string; path: string; @@ -42,13 +36,11 @@ const tabs: TabCategory[] = [ }, { category: '계정 관리', - items: [ - { label: '비밀번호 수정', path: '/admin/account-edit' }, - ], + items: [{ label: '비밀번호 수정', path: '/admin/account-edit' }], }, ]; -const SideBar = ({ clubLogo, clubName }: SideBarProps) => { +const SideBar = () => { const trackEvent = useMixpanelTrack(); const location = useLocation(); const navigate = useNavigate(); @@ -80,7 +72,7 @@ const SideBar = ({ clubLogo, clubName }: SideBarProps) => { } trackEvent(ADMIN_EVENT.LOGOUT_BUTTON_CLICKED); - + localStorage.removeItem('accessToken'); navigate('/admin/login', { replace: true }); } catch (error) { @@ -91,11 +83,7 @@ const SideBar = ({ clubLogo, clubName }: SideBarProps) => { return ( 설정 - - - - {clubName} - + {tabs.map((tab, tabIndex) => ( @@ -115,7 +103,7 @@ const SideBar = ({ clubLogo, clubName }: SideBarProps) => { ))} - + 로그아웃 diff --git a/frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.styles.ts b/frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.styles.ts index 947797e51..e6ac463ba 100644 --- a/frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.styles.ts +++ b/frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.styles.ts @@ -1,16 +1,9 @@ import styled from 'styled-components'; -export const IdInputContainer = styled.div` +export const Container = styled.div` display: flex; - flex-direction: row; - align-items: flex-end; - gap: 10px; -`; - -export const ForgotPasswordText = styled.h3` - color: #cdcdcd; - font-weight: 300; - margin-bottom: 5px; + flex-direction: column; + gap: 60px; `; export const SuccessMessage = styled.p` @@ -31,10 +24,9 @@ export const ErrorMessage = styled.p` export const GuidanceBox = styled.div` padding: 16px; - margin-bottom: 24px; /* 입력 필드와의 간격 */ - background-color: #f8f9fa; /* 부드러운 배경색 */ - border-radius: 8px; /* 둥근 모서리 */ - border: 1px solid #e9ecef; /* 옅은 테두리 */ + background-color: #f8f9fa; + border-radius: 8px; + border: 1px solid #e9ecef; `; export const GuidanceText = styled.p` diff --git a/frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.tsx b/frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.tsx index f7df36d7f..2b506f80c 100644 --- a/frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/AccountEditTab/AccountEditTab.tsx @@ -6,6 +6,7 @@ import { changePassword } from '@/apis/auth/changePassword'; import useMixpanelTrack from '@/hooks/useMixpanelTrack'; import { ADMIN_EVENT, PAGE_VIEW } from '@/constants/eventName'; import useTrackPageView from '@/hooks/useTrackPageView'; +import { ContentSection } from '@/pages/AdminPage/components/ContentSection/ContentSection'; const PASSWORD_REGEX = /^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^])(?!.*\s).{8,20}$/; @@ -65,62 +66,63 @@ const AccountEditTab = () => { }; return ( -
-

비밀번호 수정

-
- - - 비밀번호는 영문, 숫자, 특수문자(!@#$%^)를 포함하여 8자 이상 20자 이하로 입력해야 합니다. - - - 비밀번호를 잊으신 경우 모아동 관리자에게 연락 주세요. - - - - setNewPassword(e.target.value)} - onClear={() => { - setNewPassword(''); - trackEvent(ADMIN_EVENT.NEW_PASSWORD_CLEAR_BUTTON_CLICKED); - }} - maxLength={20} - isError={isPasswordValid} - isSuccess={newPassword.length > 0 && !isPasswordValid} - helperText={isPasswordValid ? '영문, 숫자, 특수문자 포함 8~20자' : ''} - /> -
- - setConfirmPassword(e.target.value)} - onClear={() => { - setConfirmPassword(''); - trackEvent(ADMIN_EVENT.CONFIRM_PASSWORD_CLEAR_BUTTON_CLICKED); - }} - maxLength={20} - isError={isPasswordMatching} - isSuccess={confirmPassword.length > 0 && !isPasswordMatching} - helperText={isPasswordMatching ? '비밀번호가 일치하지 않습니다.' : ''} - /> -
- - {successMessage && {successMessage}} -
- - -
+ + + + + + + + 비밀번호는 영문, 숫자, 특수문자(!@#$%^)를 포함하여 8자 이상 20자 이하로 입력해야 합니다. + + + 비밀번호를 잊으신 경우 모아동 관리자에게 연락 주세요. + + + + setNewPassword(e.target.value)} + onClear={() => { + setNewPassword(''); + trackEvent(ADMIN_EVENT.NEW_PASSWORD_CLEAR_BUTTON_CLICKED); + }} + maxLength={20} + isError={isPasswordValid} + isSuccess={newPassword.length > 0 && !isPasswordValid} + helperText={isPasswordValid ? '영문, 숫자, 특수문자 포함 8~20자' : ''} + /> + + setConfirmPassword(e.target.value)} + onClear={() => { + setConfirmPassword(''); + trackEvent(ADMIN_EVENT.CONFIRM_PASSWORD_CLEAR_BUTTON_CLICKED); + }} + maxLength={20} + isError={isPasswordMatching} + isSuccess={confirmPassword.length > 0 && !isPasswordMatching} + helperText={isPasswordMatching ? '비밀번호가 일치하지 않습니다.' : ''} + /> + + {successMessage && {successMessage}} + + + + + ); }; diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsx index 2ed56cef5..caf201222 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsListTab/ApplicantsListTab.tsx @@ -11,6 +11,7 @@ import { useQueryClient } from '@tanstack/react-query'; import styled from 'styled-components'; import ApplicationRowItem from '@/pages/AdminPage/components/ApplicationRow/ApplicationRowItem'; import expandArrow from '@/assets/images/icons/ExpandArrow.svg'; +import { ContentSection } from '@/pages/AdminPage/components/ContentSection/ContentSection'; const ApplicationListTab = () => { const {data: allforms, isLoading, isError, error} = useGetApplicationlist(); @@ -116,7 +117,9 @@ const ApplicationListTab = () => { return ( - 지원서 목록 +
+ +
게시된 지원서 diff --git a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx index f538fca29..cb540cb28 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicantsTab/ApplicantsTab.tsx @@ -13,6 +13,7 @@ import { useUpdateApplicant } from '@/hooks/queries/applicants/useUpdateApplican import { AVAILABLE_STATUSES } from '@/constants/status'; import { CustomDropDown } from '@/components/common/CustomDropDown/CustomDropDown'; import { useGetApplicants } from '@/hooks/queries/applicants/useGetApplicants'; +import { ContentSection } from '@/pages/AdminPage/components/ContentSection/ContentSection'; const ApplicantsTab = () => { const { applicationFormId } = useParams<{ applicationFormId: string }>(); @@ -228,14 +229,9 @@ const ApplicantsTab = () => { return ( <> - - 지원 현황 - {/* - - - - */} - +
+ +
diff --git a/frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsx b/frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsx index 6220a6f88..2c05e49d8 100644 --- a/frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ApplicationListTab/ApplicationListTab.tsx @@ -12,6 +12,7 @@ import { updateApplicationStatus } from '@/apis/application/updateApplication'; import { useQueryClient } from '@tanstack/react-query'; import styled from 'styled-components'; import ApplicationRowItem from '@/pages/AdminPage/components/ApplicationRow/ApplicationRowItem'; +import { ContentSection } from '@/pages/AdminPage/components/ContentSection/ContentSection'; const ApplicationListTab = () => { const { data: allforms, isLoading, isError, error } = useGetApplicationlist(); @@ -117,7 +118,9 @@ const ApplicationListTab = () => { return ( - 지원서 목록 +
+ +
게시된 지원서 diff --git a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.styles.ts b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.styles.ts index e76d17c4f..408264a16 100644 --- a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.styles.ts +++ b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.styles.ts @@ -1,23 +1,15 @@ import styled from 'styled-components'; -export const TitleButtonContainer = styled.div` +export const Container = styled.div` display: flex; - flex-direction: row; - justify-content: space-between; -`; - -export const InfoTitle = styled.h2` - font-size: 1.5rem; - font-weight: bold; - letter-spacing: 0; - margin-bottom: 30px; + flex-direction: column; + gap: 60px; `; -export const InfoGroup = styled.div` +export const FieldGroup = styled.div` display: flex; flex-direction: column; gap: 30px; - margin-bottom: 140px; `; export const PresidentContainer = styled.div` @@ -27,20 +19,6 @@ export const PresidentContainer = styled.div` max-width: 700px; `; -export const TagEditGroup = styled.div` - display: flex; - flex-direction: column; - gap: 30px; - margin-bottom: 120px; -`; - -export const SNSInputGroup = styled.div` - display: flex; - flex-direction: column; - gap: 30px; - margin-top: 30px; -`; - export const SNSRow = styled.div` display: flex; flex-direction: column; @@ -49,7 +27,7 @@ export const SNSRow = styled.div` align-items: flex-start; `; -export const SNSCheckboxLabel = styled.label` +export const SNSLabel = styled.label` display: flex; align-items: center; gap: 10px; diff --git a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx index 2eeb9a043..ffbdd2937 100644 --- a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx @@ -1,8 +1,7 @@ import { useState, useEffect } from 'react'; import { useOutletContext } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; -import { ClubDetail } from '@/types/club'; -import { SNSPlatform } from '@/types/club'; +import { ClubDetail, SNSPlatform } from '@/types/club'; import { useUpdateClubDetail } from '@/hooks/queries/club/useUpdateClubDetail'; import { validateSocialLink } from '@/utils/validateSocialLink'; import { SNS_CONFIG } from '@/constants/snsConfig'; @@ -10,10 +9,14 @@ import InputField from '@/components/common/InputField/InputField'; import Button from '@/components/common/Button/Button'; import SelectTags from '@/pages/AdminPage/tabs/ClubInfoEditTab/components/SelectTags/SelectTags'; import MakeTags from '@/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags'; -import * as Styled from './ClubInfoEditTab.styles'; +import ClubLogoEditor from '@/pages/AdminPage/components/ClubLogoEditor/ClubLogoEditor'; + import { ADMIN_EVENT, PAGE_VIEW } from '@/constants/eventName'; import useMixpanelTrack from '@/hooks/useMixpanelTrack'; import useTrackPageView from '@/hooks/useTrackPageView'; +import { ContentSection } from '@/pages/AdminPage/components/ContentSection/ContentSection'; +import * as Styled from './ClubInfoEditTab.styles'; + const ClubInfoEditTab = () => { const trackEvent = useMixpanelTrack(); @@ -118,118 +121,102 @@ const ClubInfoEditTab = () => { }; return ( - <> - - 동아리 기본 정보 수정 - - - - - setClubName(e.target.value)} - onClear={() => { - trackEvent(ADMIN_EVENT.CLUB_NAME_CLEAR_BUTTON_CLICKED); - setClubName(''); - }} - width='40%' - maxLength={10} - showMaxChar={true} + + + + 저장하기 + + } /> - + + setClubPresidentName(e.target.value)} + label='동아리명' + placeholder='동아리명' + value={clubName} + onChange={(e) => setClubName(e.target.value)} onClear={() => { - trackEvent(ADMIN_EVENT.CLUB_PRESIDENT_CLEAR_BUTTON_CLICKED); - setClubPresidentName(''); + trackEvent(ADMIN_EVENT.CLUB_NAME_CLEAR_BUTTON_CLICKED); + setClubName(''); }} - maxLength={5} + width='40%' + maxLength={20} + showMaxChar={true} /> setTelephoneNumber(e.target.value)} + maxLength={20} + showMaxChar={true} + value={introduction} + onChange={(e) => setIntroduction(e.target.value)} onClear={() => { - trackEvent(ADMIN_EVENT.TELEPHONE_NUMBER_CLEAR_BUTTON_CLICKED); - setTelephoneNumber(''); + trackEvent(ADMIN_EVENT.CLUB_INTRODUCTION_CLEAR_BUTTON_CLICKED); + setIntroduction(''); }} /> - - - - 동아리 태그 수정 - - setIntroduction(e.target.value)} - onClear={() => { - trackEvent(ADMIN_EVENT.CLUB_INTRODUCTION_CLEAR_BUTTON_CLICKED); - setIntroduction(''); - }} - /> - + - + - - - - 동아리 SNS 링크 - - {Object.entries(SNS_CONFIG).map(([rawKey, { label, placeholder }]) => { - const key = rawKey as SNSPlatform; - - return ( - - {label} - handleSocialLinkChange(key, e.target.value)} - onClear={() => { - trackEvent(ADMIN_EVENT.CLUB_SNS_LINK_CLEAR_BUTTON_CLICKED, { - snsPlatform: label, - }); - setSocialLinks((prev) => ({ ...prev, [key]: '' })); - setSnsErrors((prev) => ({ ...prev, [key]: '' })); - }} - isError={snsErrors[key] !== ''} - helperText={snsErrors[key]} - /> - - ); - })} - - + + + + + + + + + {Object.entries(SNS_CONFIG).map( + ([rawKey, { label, placeholder }]) => { + const key = rawKey as SNSPlatform; + + return ( + + {label} + + handleSocialLinkChange(key, e.target.value) + } + onClear={() => { + trackEvent( + ADMIN_EVENT.CLUB_SNS_LINK_CLEAR_BUTTON_CLICKED, + { + snsPlatform: label, + }, + ); + setSocialLinks((prev) => ({ ...prev, [key]: '' })); + setSnsErrors((prev) => ({ ...prev, [key]: '' })); + }} + isError={snsErrors[key] !== ''} + helperText={snsErrors[key]} + /> + + ); + }, + )} + + +
); }; diff --git a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.styles.ts b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.styles.ts index feb823e7d..a720a6907 100644 --- a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.styles.ts +++ b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.styles.ts @@ -1,5 +1,4 @@ import styled from 'styled-components'; -import deleteIcon from '@/assets/images/icons/delete_button_icon.svg'; export const Label = styled.label` display: block; @@ -36,19 +35,32 @@ export const TagTextInput = styled.input` background: transparent; font-size: 0.875rem; color: #4b4b4b; - width: 110px; - padding-right: 10px; + width: 100px; + padding-right: 30px; `; export const RemoveButton = styled.button` position: absolute; top: 50%; - right: 10px; + right: 8px; transform: translateY(-50%); - width: 16px; - height: 16px; - background: url(${deleteIcon}) no-repeat center; - background-size: contain; + width: 20px; + height: 20px; + padding: 0; + background: transparent; border: none; cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + + img { + width: 16px; + height: 16px; + opacity: 0.5; + } + + &:hover img { + opacity: 1; + } `; diff --git a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.tsx b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.tsx index 9243e3ff8..82de909ab 100644 --- a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.tsx +++ b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.tsx @@ -1,6 +1,7 @@ import * as Styled from './MakeTags.styles'; import useMixpanelTrack from '@/hooks/useMixpanelTrack'; import { ADMIN_EVENT } from '@/constants/eventName'; +import deleteButton from '@/assets/images/icons/delete_button_icon.svg'; interface MakeTagsProps { value: string[]; @@ -29,7 +30,7 @@ const MakeTags = ({ value, onChange }: MakeTagsProps) => { } return tag; }); - + trackEvent(ADMIN_EVENT.CLUB_TAG_CLEAR_BUTTON_CLICKED, { tagIndex: index + 1, }); @@ -48,9 +49,17 @@ const MakeTags = ({ value, onChange }: MakeTagsProps) => { value={tag} maxLength={5} onChange={(e) => updateTag(index, e.target.value)} + placeholder={`자유 태그 ${index + 1}`} + aria-label={`자유 태그 ${index + 1}`} /> {tag.length > 0 && ( - clearTag(index)} /> + clearTag(index)} + aria-label={`자유 태그 ${index + 1} 삭제`} + type='button' + > + + )} ))} diff --git a/frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.styles.ts b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.styles.ts index 0a4a9d93a..a8c74b814 100644 --- a/frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.styles.ts +++ b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.styles.ts @@ -1,16 +1,9 @@ import styled from 'styled-components'; -export const PhotoEditorContainer = styled.div` +export const Container = styled.div` display: flex; flex-direction: column; - width: 100%; -`; - -export const ImageContainer = styled.div` - display: flex; - flex-direction: column; - gap: 15px; - overflow: hidden; + gap: 60px; `; export const ImageGrid = styled.div` @@ -21,15 +14,8 @@ export const ImageGrid = styled.div` max-width: 770px; `; -export const InfoTitle = styled.h2` - font-size: 1.5rem; - font-weight: bold; - letter-spacing: 0; - margin-bottom: 46px; -`; - export const Label = styled.p` font-size: 1.125rem; - margin-bottom: 12px; + margin-bottom: 8px; font-weight: 600; `; diff --git a/frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx index d67a10268..4aaf23268 100644 --- a/frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx @@ -1,11 +1,15 @@ import { useEffect, useState, useRef } from 'react'; import { useOutletContext } from 'react-router-dom'; + import * as Styled from './PhotoEditTab.styles'; import { ImagePreview } from '@/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview'; +import { ContentSection } from '@/pages/AdminPage/components/ContentSection/ContentSection'; + import { ClubDetail } from '@/types/club'; import useMixpanelTrack from '@/hooks/useMixpanelTrack'; -import { ADMIN_EVENT, PAGE_VIEW } from '@/constants/eventName'; import useTrackPageView from '@/hooks/useTrackPageView'; +import { ADMIN_EVENT, PAGE_VIEW } from '@/constants/eventName'; + import { useUploadFeed, useUpdateFeed, @@ -18,8 +22,10 @@ const PhotoEditTab = () => { useTrackPageView(PAGE_VIEW.PHOTO_EDIT_PAGE); const clubDetail = useOutletContext(); - const { mutate: updateFeed, isPending: isUpdating } = useUpdateFeed(); + const { mutate: uploadFeed, isPending: isUploading } = useUploadFeed(); + const { mutate: updateFeed, isPending: isUpdating } = useUpdateFeed(); + const [imageList, setImageList] = useState([]); const inputRef = useRef(null); @@ -33,83 +39,103 @@ const PhotoEditTab = () => { const handleFiles = (files: FileList | null) => { if (!files || files.length === 0) return; - const fileArray = Array.from(files); uploadFeed({ clubId: clubDetail.id, - files: fileArray, + files: Array.from(files), existingUrls: imageList, }); }; const handleUploadClick = () => { if (isLoading) return; + trackEvent(ADMIN_EVENT.IMAGE_UPLOAD_BUTTON_CLICKED); + if (imageList.length >= MAX_FILE_COUNT) { alert(`이미지는 최대 ${MAX_FILE_COUNT}장까지만 업로드할 수 있어요.`); return; } + inputRef.current?.click(); }; + /** 파일 선택 변경 */ + const handleFileChange = (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files || files.length === 0) return; + + const oversizedFile = Array.from(files).find( + (file) => file.size > MAX_FILE_SIZE, + ); + + if (oversizedFile) { + alert( + `선택한 사진 중 ${oversizedFile.name}의 용량이 제한을 초과했습니다.`, + ); + e.target.value = ''; + return; + } + + handleFiles(files); + }; + + /** 이미지 삭제 */ const deleteImage = (index: number) => { if (isLoading) return; + const newList = imageList.filter((_, i) => i !== index); setImageList(newList); - updateFeed({ clubId: clubDetail.id, urls: newList }); + + updateFeed({ + clubId: clubDetail.id, + urls: newList, + }); }; return ( - - 활동 사진 편집 - - 활동사진 추가 (최대 5장) - -
- { - const files = e.target.files; - if (!files || files.length === 0) return; - - const oversizedFile = Array.from(files).find( - (file) => file.size > MAX_FILE_SIZE, - ); - if (oversizedFile) { - alert( - `선택한 사진 중 ${oversizedFile.name}의 용량이 10MB를 초과합니다.`, - ); - e.target.value = ''; - return; - } - handleFiles(files); - }} - /> - -
- -
- 활동사진 수정 - - {imageList.map((image, index) => ( - { - trackEvent(ADMIN_EVENT.IMAGE_DELETE_BUTTON_CLICKED); - deleteImage(index); - }} - disabled={isLoading} + + + + + + 활동사진 추가 (최대 {MAX_FILE_COUNT}장) + +
+ - ))} - - - + + +
+ + 활동사진 수정 + + {imageList.map((image, index) => ( + { + trackEvent(ADMIN_EVENT.IMAGE_DELETE_BUTTON_CLICKED); + deleteImage(index); + }} + /> + ))} + +
+
+
); }; diff --git a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.styles.ts b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.styles.ts index c85951fe0..d08edcd0a 100644 --- a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.styles.ts +++ b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.styles.ts @@ -1,40 +1,21 @@ import styled from 'styled-components'; -export const RecruitEditorContainer = styled.div` +export const Container = styled.div` display: flex; flex-direction: column; -`; - -export const TitleButtonContainer = styled.div` - display: flex; - flex-direction: row; - justify-content: space-between; -`; - -export const InfoTitle = styled.h2` - font-size: 1.5rem; - font-weight: bold; - letter-spacing: 0; - margin-bottom: 46px; -`; - -export const InfoGroup = styled.div` - display: flex; - flex-direction: column; - gap: 30px; - margin-bottom: 140px; + gap: 60px; `; export const RecruitPeriodContainer = styled.div` - display: flex; - gap: 16px; + display: flex; + gap: 16px; max-width: 706px; `; export const AlwaysRecruitButton = styled.button<{ $active: boolean }>` border-radius: 6px; height: 45px; - padding: 0px 16px ; + padding: 0px 16px; font-weight: 600; font-size: 1rem; cursor: pointer; @@ -43,15 +24,17 @@ export const AlwaysRecruitButton = styled.button<{ $active: boolean }>` color: ${({ $active }) => ($active ? '#fff' : '#797979')}; background: ${({ $active }) => ($active ? '#FF7543' : 'rgba(0,0,0,0.05)')}; border: ${({ $active }) => ($active ? 'none' : '1px solid #C5C5C5')}; - transition: background-color 0.12s ease, transform 0.06s ease; + transition: + background-color 0.12s ease, + transform 0.06s ease; &:active { - transform: translateY(1px); + transform: translateY(1px); } `; export const Label = styled.p` font-size: 1.125rem; - margin-bottom: 12px; + margin-bottom: 8px; font-weight: 600; `; diff --git a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx index f65fbc85d..80c0873d8 100644 --- a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useOutletContext } from 'react-router-dom'; import * as Styled from './RecruitEditTab.styles'; import Calendar from '@/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar'; @@ -13,6 +13,7 @@ import { setYear } from 'date-fns'; import { ADMIN_EVENT, PAGE_VIEW } from '@/constants/eventName'; import useTrackPageView from '@/hooks/useTrackPageView'; import useMixpanelTrack from '@/hooks/useMixpanelTrack'; +import { ContentSection } from '@/pages/AdminPage/components/ContentSection/ContentSection'; const FAR_FUTURE_YEAR = 2999; @@ -136,53 +137,59 @@ const RecruitEditTab = () => { }; return ( - - - 동아리 모집 정보 수정 - - - -
- 모집 기간 설정 - - - - 상시모집 - - -
- setRecruitmentTarget(e.target.value)} - onClear={() => { - trackEvent(ADMIN_EVENT.RECRUITMENT_TARGET_CLEAR_BUTTON_CLICKED); - setRecruitmentTarget(''); - }} - maxLength={10} + + + + 저장하기 + + } /> -
- 소개글 수정 - -
-
-
+ +
+ 모집 기간 + + + + 상시모집 + + +
+ + setRecruitmentTarget(e.target.value)} + onClear={() => { + trackEvent(ADMIN_EVENT.RECRUITMENT_TARGET_CLEAR_BUTTON_CLICKED); + setRecruitmentTarget(''); + }} + maxLength={10} + /> + +
+ 소개글 + +
+
+ +
); }; export default RecruitEditTab;