From c9837f24fd21c21bfa75262665321e56f2d05ba3 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:46:03 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor(admin):=20InfoSection=EC=9D=84=20?= =?UTF-8?q?=EC=96=B4=EB=93=9C=EB=AF=BC=20=EA=B3=B5=EC=9A=A9=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mobile => components}/InfoSection/InfoSection.stories.tsx | 2 +- .../mobile => components}/InfoSection/InfoSection.styles.ts | 0 .../mobile => components}/InfoSection/InfoSection.tsx | 0 .../AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsx | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename frontend/src/pages/AdminPage/{tabs/ClubIntroEditTab/components/mobile => components}/InfoSection/InfoSection.stories.tsx (97%) rename frontend/src/pages/AdminPage/{tabs/ClubIntroEditTab/components/mobile => components}/InfoSection/InfoSection.styles.ts (100%) rename frontend/src/pages/AdminPage/{tabs/ClubIntroEditTab/components/mobile => components}/InfoSection/InfoSection.tsx (100%) diff --git a/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/InfoSection/InfoSection.stories.tsx b/frontend/src/pages/AdminPage/components/InfoSection/InfoSection.stories.tsx similarity index 97% rename from frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/InfoSection/InfoSection.stories.tsx rename to frontend/src/pages/AdminPage/components/InfoSection/InfoSection.stories.tsx index a4e96babc..beb3af5a7 100644 --- a/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/InfoSection/InfoSection.stories.tsx +++ b/frontend/src/pages/AdminPage/components/InfoSection/InfoSection.stories.tsx @@ -53,7 +53,7 @@ const MobileTextarea = ({ }; const meta = { - title: 'Pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/InfoSection', + title: 'Pages/AdminPage/components/InfoSection', component: InfoSection, parameters: { layout: 'centered' }, tags: ['autodocs'], diff --git a/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/InfoSection/InfoSection.styles.ts b/frontend/src/pages/AdminPage/components/InfoSection/InfoSection.styles.ts similarity index 100% rename from frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/InfoSection/InfoSection.styles.ts rename to frontend/src/pages/AdminPage/components/InfoSection/InfoSection.styles.ts diff --git a/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/InfoSection/InfoSection.tsx b/frontend/src/pages/AdminPage/components/InfoSection/InfoSection.tsx similarity index 100% rename from frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/components/mobile/InfoSection/InfoSection.tsx rename to frontend/src/pages/AdminPage/components/InfoSection/InfoSection.tsx diff --git a/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsx b/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsx index 5d59ee004..934be0055 100644 --- a/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsx +++ b/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsx @@ -20,7 +20,7 @@ import * as Styled from './ClubIntroEditTabMobile.styles'; import AwardEditPage from './components/mobile/AwardEditPage/AwardEditPage'; import AwardSection from './components/mobile/AwardSection/AwardSection'; import FAQSection from './components/mobile/FAQSection/FAQSection'; -import InfoSection from './components/mobile/InfoSection/InfoSection'; +import InfoSection from '@/pages/AdminPage/components/InfoSection/InfoSection'; interface ClubIntroEditTabMobileProps { introDescription: string; From aacefad6e0424f78147820b086caf81ad02cbfc0 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:46:14 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(admin):=20=EB=AA=A8=EC=A7=91=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95=20=ED=83=AD=20=EB=AA=A8?= =?UTF-8?q?=EB=B0=94=EC=9D=BC=20=ED=99=94=EB=A9=B4=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tabs/RecruitEditTab/RecruitEditTab.tsx | 54 +++++++- .../RecruitEditTabMobile.styles.ts | 111 ++++++++++++++++ .../RecruitEditTab/RecruitEditTabMobile.tsx | 119 ++++++++++++++++++ 3 files changed, 280 insertions(+), 4 deletions(-) create mode 100644 frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTabMobile.styles.ts create mode 100644 frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTabMobile.tsx diff --git a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx index 1e68c7146..f0ccb7752 100644 --- a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx @@ -1,6 +1,5 @@ import { useEffect, useRef, useState } from 'react'; import { useOutletContext } from 'react-router-dom'; -import { useQueryClient } from '@tanstack/react-query'; import { setYear } from 'date-fns'; import Button from '@/components/common/Button/Button'; import InputField from '@/components/common/InputField/InputField'; @@ -9,15 +8,18 @@ import { ADMIN_EVENT, PAGE_VIEW } from '@/constants/eventName'; import useMixpanelTrack from '@/hooks/Mixpanel/useMixpanelTrack'; import useTrackPageView from '@/hooks/Mixpanel/useTrackPageView'; import { useUpdateClubDescription } from '@/hooks/Queries/useClub'; +import useDevice from '@/hooks/useDevice'; import { ContentSection } from '@/pages/AdminPage/components/ContentSection/ContentSection'; import { ClubDetail } from '@/types/club'; import { recruitmentDateParser } from '@/utils/recruitmentDateParser'; import DateTimeRangePicker from './components/DateTimeRangePicker/DateTimeRangePicker'; +import RecruitEditTabMobile from './RecruitEditTabMobile'; import * as Styled from './RecruitEditTab.styles'; const FAR_FUTURE_YEAR = 2999; const RecruitEditTab = () => { + const { isMobile, isTablet } = useDevice(); const trackEvent = useMixpanelTrack(); useTrackPageView(PAGE_VIEW.RECRUITMENT_INFO_EDIT_PAGE); @@ -30,6 +32,18 @@ const RecruitEditTab = () => { const [recruitmentTarget, setRecruitmentTarget] = useState(''); const [isAlwaysRecruiting, setIsAlwaysRecruiting] = useState(false); + const [initialValues, setInitialValues] = useState<{ + recruitmentStart: string | null; + recruitmentEnd: string | null; + recruitmentTarget: string; + } | null>(null); + + const isDirty = + initialValues !== null && + (recruitmentStart?.toISOString() !== initialValues.recruitmentStart || + recruitmentEnd?.toISOString() !== initialValues.recruitmentEnd || + recruitmentTarget !== initialValues.recruitmentTarget); + const backupRangeRef = useRef<{ start: Date | null; end: Date | null }>({ start: null, end: null, @@ -59,7 +73,7 @@ const RecruitEditTab = () => { }; useEffect(() => { - if (!clubDetail) return; + if (!clubDetail || isDirty) return; const parsedStart = clubDetail.recruitmentStart ? recruitmentDateParser(clubDetail.recruitmentStart) @@ -76,6 +90,14 @@ const RecruitEditTab = () => { if (isAlways) backupRangeRef.current = { start: parsedStart, end: parsedEnd }; + + setInitialValues({ + recruitmentStart: parsedStart?.toISOString() ?? null, + recruitmentEnd: parsedEnd?.toISOString() ?? null, + recruitmentTarget: clubDetail.recruitmentTarget || '', + }); + // isDirty는 의도적으로 deps에서 제외 — clubDetail refetch 시점의 편집 상태만 확인하면 된다 + // eslint-disable-next-line react-hooks/exhaustive-deps }, [clubDetail]); useEffect(() => { @@ -113,7 +135,7 @@ const RecruitEditTab = () => { }); }; - const handleUpdateClub = async () => { + const handleUpdateClub = () => { trackEvent(ADMIN_EVENT.UPDATE_RECRUIT_BUTTON_CLICKED); if (!clubDetail) return; @@ -125,12 +147,36 @@ const RecruitEditTab = () => { }; updateClubDescription(updatedData, { - onSuccess: () => alert('모집 정보가 성공적으로 수정되었습니다.'), + onSuccess: () => { + alert('모집 정보가 성공적으로 수정되었습니다.'); + setInitialValues({ + recruitmentStart: recruitmentStart?.toISOString() ?? null, + recruitmentEnd: recruitmentEnd?.toISOString() ?? null, + recruitmentTarget, + }); + }, onError: (error) => alert(`모집 정보 수정에 실패했습니다: ${error.message}`), }); }; + if (isMobile || isTablet) { + return ( + + ); + } + return ( diff --git a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTabMobile.styles.ts b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTabMobile.styles.ts new file mode 100644 index 000000000..a43b8eaac --- /dev/null +++ b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTabMobile.styles.ts @@ -0,0 +1,111 @@ +import styled from 'styled-components'; +import { media } from '@/styles/mediaQuery'; +import { colors } from '@/styles/theme/colors'; +import { setTypography, typography } from '@/styles/theme/typography'; + +export const MobileContainer = styled.div` + display: flex; + flex-direction: column; + padding-bottom: calc(80px + env(safe-area-inset-bottom) + 40px); + width: 100%; + max-width: 500px; + min-height: 100vh; + margin: 0 auto; + box-shadow: 0px 2px 12px rgba(0, 0, 0, 0.04); + + ${media.mobile} { + max-width: 100%; + margin: 0; + box-shadow: none; + } +`; + +export const FormSection = styled.div` + display: flex; + flex-direction: column; + gap: 30px; + padding: 32px 20px 0; +`; + +export const PageTitle = styled.h2` + ${setTypography(typography.title.title5)} + color: ${colors.base.black}; + margin: 0; +`; + +export const PageSubtitle = styled.p` + ${setTypography(typography.button.button1)} + color: ${colors.gray[700]}; + margin: -22px 0 0; +`; + +export const FieldList = styled.div` + display: flex; + flex-direction: column; + gap: 16px; +`; + +export const PeriodSection = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; + +export const SectionLabel = styled.span` + ${setTypography(typography.button.button1)} + color: ${colors.gray[900]}; + padding: 0 2px; +`; + +export const DateTimeRow = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; + +export const DateTimeInput = styled.input<{ $isDisabled?: boolean }>` + width: 100%; + height: 45px; + padding: 0 14px; + border: none; + border-radius: 12px; + background-color: ${({ $isDisabled }) => + $isDisabled ? colors.gray[400] : colors.gray[200]}; + color: ${({ $isDisabled }) => + $isDisabled ? colors.gray[500] : colors.gray[700]}; + font-size: 1rem; + pointer-events: ${({ $isDisabled }) => ($isDisabled ? 'none' : 'auto')}; + + &::-webkit-calendar-picker-indicator { + ${({ $isDisabled }) => $isDisabled && `filter: invert(77%);`} + } +`; + +export const AlwaysRecruitButton = styled.button<{ $isAlwaysActive: boolean }>` + align-self: flex-start; + border-radius: 10px; + padding: 0 18px; + height: 40px; + ${setTypography(typography.button.button1)} + cursor: pointer; + + color: ${colors.gray[700]}; + background-color: ${colors.gray[300]}; + border: 1px solid ${colors.gray[400]}; + + ${({ $isAlwaysActive }) => + $isAlwaysActive && + ` + color: ${colors.base.white}; + background-color: ${colors.primary[800]}; + border: none; + `} + + transition: + background-color 0.12s ease, + transform 0.06s ease; + + &:active { + transform: translateY(1px); + } +`; diff --git a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTabMobile.tsx b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTabMobile.tsx new file mode 100644 index 000000000..cc54d104d --- /dev/null +++ b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTabMobile.tsx @@ -0,0 +1,119 @@ +import { useNavigate } from 'react-router-dom'; +import FixedBottomButtonArea from '@/components/common/FixedBottomButtonArea/FixedBottomButtonArea'; +import WebviewTopBar from '@/components/common/WebviewTopBar/WebviewTopBar'; +import { RECRUIT_TARGET_MAX } from '@/constants/adminFieldLimits'; +import ClearableTextArea from '@/pages/AdminPage/components/ClearableTextArea/ClearableTextArea'; +import InfoSection from '@/pages/AdminPage/components/InfoSection/InfoSection'; +import * as Styled from './RecruitEditTabMobile.styles'; + +const toDateTimeLocalValue = (date: Date | null): string => { + if (!date) return ''; + const pad = (n: number) => String(n).padStart(2, '0'); + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; +}; + +const fromDateTimeLocalValue = (value: string): Date | null => { + if (!value) return null; + return new Date(value); +}; + +interface RecruitEditTabMobileProps { + recruitmentStart: Date | null; + recruitmentEnd: Date | null; + recruitmentTarget: string; + isAlwaysRecruiting: boolean; + isDirty: boolean; + onStartChange: (date: Date | null) => void; + onEndChange: (date: Date | null) => void; + onTargetChange: (value: string) => void; + onToggleAlwaysRecruiting: () => void; + onSave: () => void; +} + +const RecruitEditTabMobile = ({ + recruitmentStart, + recruitmentEnd, + recruitmentTarget, + isAlwaysRecruiting, + isDirty, + onStartChange, + onEndChange, + onTargetChange, + onToggleAlwaysRecruiting, + onSave, +}: RecruitEditTabMobileProps) => { + const navigate = useNavigate(); + + return ( + <> + + navigate('/admin')} + /> + + + 모집 정보를 입력해주세요 + 해당 기간 동안 모집중 상태가 돼요 + + + + 모집 기간 + + + onStartChange(fromDateTimeLocalValue(e.target.value)) + } + /> + + onEndChange(fromDateTimeLocalValue(e.target.value)) + } + $isDisabled={isAlwaysRecruiting} + /> + + + 상시모집 + + + + + onTargetChange('')} + placeholder='모집대상을 입력해주세요' + maxLength={RECRUIT_TARGET_MAX} + /> + + + + + + + 저장하기 + + + ); +}; + +export default RecruitEditTabMobile; From 4118ef51a80fd02d6326794b49ee22e894a1ae72 Mon Sep 17 00:00:00 2001 From: suhyun113 <163711629+suhyun113@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:52:24 +0900 Subject: [PATCH 3/3] =?UTF-8?q?style:=20prettier=20=ED=8F=AC=EB=A7=B7?= =?UTF-8?q?=EC=9D=84=20=EC=A0=81=EC=9A=A9=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsx | 2 +- .../pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx | 2 +- .../AdminPage/tabs/RecruitEditTab/RecruitEditTabMobile.tsx | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsx b/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsx index 934be0055..c50ab01a7 100644 --- a/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsx +++ b/frontend/src/pages/AdminPage/tabs/ClubIntroEditTab/ClubIntroEditTabMobile.tsx @@ -15,12 +15,12 @@ import { INTRO_DESCRIPTION_PLACEHOLDER, } from '@/constants/adminFieldPlaceholders'; import ClearableTextArea from '@/pages/AdminPage/components/ClearableTextArea/ClearableTextArea'; +import InfoSection from '@/pages/AdminPage/components/InfoSection/InfoSection'; import { Award, FAQ, IdealCandidate } from '@/types/club'; import * as Styled from './ClubIntroEditTabMobile.styles'; import AwardEditPage from './components/mobile/AwardEditPage/AwardEditPage'; import AwardSection from './components/mobile/AwardSection/AwardSection'; import FAQSection from './components/mobile/FAQSection/FAQSection'; -import InfoSection from '@/pages/AdminPage/components/InfoSection/InfoSection'; interface ClubIntroEditTabMobileProps { introDescription: string; diff --git a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx index f0ccb7752..7196d7ca8 100644 --- a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx @@ -13,8 +13,8 @@ import { ContentSection } from '@/pages/AdminPage/components/ContentSection/Cont import { ClubDetail } from '@/types/club'; import { recruitmentDateParser } from '@/utils/recruitmentDateParser'; import DateTimeRangePicker from './components/DateTimeRangePicker/DateTimeRangePicker'; -import RecruitEditTabMobile from './RecruitEditTabMobile'; import * as Styled from './RecruitEditTab.styles'; +import RecruitEditTabMobile from './RecruitEditTabMobile'; const FAR_FUTURE_YEAR = 2999; diff --git a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTabMobile.tsx b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTabMobile.tsx index cc54d104d..ee50bf3c7 100644 --- a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTabMobile.tsx +++ b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTabMobile.tsx @@ -54,7 +54,9 @@ const RecruitEditTabMobile = ({ 모집 정보를 입력해주세요 - 해당 기간 동안 모집중 상태가 돼요 + + 해당 기간 동안 모집중 상태가 돼요 +