diff --git a/.github/workflows/jira-integration.yml b/.github/workflows/jira-integration.yml new file mode 100644 index 0000000..7b26d44 --- /dev/null +++ b/.github/workflows/jira-integration.yml @@ -0,0 +1,43 @@ +name: Jira Integration + +on: + issues: + types: [opened, closed] + +jobs: + jira-integration: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Create Jira Issue + if: github.event.action == 'opened' + uses: atlassian/gajira-create-issue@v3 + with: + issueType: '10001' # Story + projectKey: 'DASOMFE' + summary: ${{ github.event.issue.title }} + description: | + GitHub Issue: ${{ github.event.issue.html_url }} + + ${{ github.event.issue.body }} + fields: | + { + "customfield_10014": "${{ github.event.issue.number }}" + } + env: + JIRA_URL: ${{ secrets.JIRA_URL }} + JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + + - name: Close Jira Issue + if: github.event.action == 'closed' + uses: atlassian/gajira-transition-issue@v3 + with: + issueKey: DASOMFE-${{ github.event.issue.number }} + transition: 'Done' + env: + JIRA_URL: ${{ secrets.JIRA_URL }} + JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TO KEN }} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..17951ca --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + "semi": false, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "avoid", + "endOfLine": "lf", + "jsxSingleQuote": true +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 42dffe3..6c2edd8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,11 @@ import React from 'react' import './assets/styles/index.css' -import { BrowserRouter as Router, Routes, Route, useLocation } from 'react-router-dom' +import { + BrowserRouter as Router, + Routes, + Route, + useLocation, +} from 'react-router-dom' import Main from './pages/Main' import AdminMain from './pages/admin/AdminMain' import Login from './pages/Login' @@ -30,54 +35,120 @@ import ProtectedRoute from './components/layout/ProtectRoute' import SomkatonApplicants from './pages/admin/SomkatonApplicants' function App() { - return ( -
- - - -
- ) + return ( +
+ + + +
+ ) } function AppContent() { - const location = useLocation() - const hideHeader = ['/login'] // 헤더를 숨길 페이지 + const location = useLocation() + const hideHeader = ['/login'] // 헤더를 숨길 페이지 - return ( - <> - {/* 지정한 페이지 header 숨기기 */} - {!hideHeader.includes(location.pathname) &&
} - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + return ( + <> + {/* 지정한 페이지 header 숨기기 */} + {!hideHeader.includes(location.pathname) &&
} + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + } /> + } /> - {/* 관리자 페이지 */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - }/> - - - ) + {/* 관리자 페이지 */} + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + ) } export default App diff --git a/src/assets/styles/index.css b/src/assets/styles/index.css index c33b670..1d0abb9 100644 --- a/src/assets/styles/index.css +++ b/src/assets/styles/index.css @@ -3,119 +3,119 @@ @tailwind utilities; @font-face { - font-family: "Pretendard Black"; - src: url("/public/fonts/Pretendard-Black.woff") format("woff"); + font-family: 'Pretendard Black'; + src: url('/public/fonts/Pretendard-Black.woff') format('woff'); } @font-face { - font-family: "Pretendard Bold"; - src: url("/public/fonts/Pretendard-Bold.woff") format("woff"); + font-family: 'Pretendard Bold'; + src: url('/public/fonts/Pretendard-Bold.woff') format('woff'); } @font-face { - font-family: "Pretendard ExtraBold"; - src: url("/public/fonts/Pretendard-ExtraBold.woff") format("woff"); + font-family: 'Pretendard ExtraBold'; + src: url('/public/fonts/Pretendard-ExtraBold.woff') format('woff'); } @font-face { - font-family: "Pretendard ExtraLight"; - src: url("/public/fonts/Pretendard-ExtraLight.woff") format("woff"); + font-family: 'Pretendard ExtraLight'; + src: url('/public/fonts/Pretendard-ExtraLight.woff') format('woff'); } @font-face { - font-family: "Pretendard Light"; - src: url("/public/fonts/Pretendard-Light.woff") format("woff"); + font-family: 'Pretendard Light'; + src: url('/public/fonts/Pretendard-Light.woff') format('woff'); } @font-face { - font-family: "Pretendard Medium"; - src: url("/public/fonts/Pretendard-Medium.woff") format("woff"); + font-family: 'Pretendard Medium'; + src: url('/public/fonts/Pretendard-Medium.woff') format('woff'); } @font-face { - font-family: "Pretendard Regular"; - src: url("/public/fonts/Pretendard-Regular.woff") format("woff"); + font-family: 'Pretendard Regular'; + src: url('/public/fonts/Pretendard-Regular.woff') format('woff'); } @font-face { - font-family: "Pretendard SemiBold"; - src: url("/public/fonts/Pretendard-SemiBold.woff") format("woff"); + font-family: 'Pretendard SemiBold'; + src: url('/public/fonts/Pretendard-SemiBold.woff') format('woff'); } @font-face { - font-family: "Pretendard Thin"; - src: url("/public/fonts/Pretendard-Thin.woff") format("woff"); + font-family: 'Pretendard Thin'; + src: url('/public/fonts/Pretendard-Thin.woff') format('woff'); } @layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 47.4% 11.2%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --card: 0 0% 100%; - --card-foreground: 222.2 47.4% 11.2%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 100% 50%; - --destructive-foreground: 210 40% 98%; - --ring: 215 20.2% 65.1%; - --radius: 0.5rem; - } - - .dark { - --background: 224 71% 4%; - --foreground: 213 31% 91%; - --muted: 223 47% 11%; - --muted-foreground: 215.4 16.3% 56.9%; - --accent: 216 34% 17%; - --accent-foreground: 210 40% 98%; - --popover: 224 71% 4%; - --popover-foreground: 215 20.2% 65.1%; - --border: 216 34% 17%; - --input: 216 34% 17%; - --card: 224 71% 4%; - --card-foreground: 213 31% 91%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 1.2%; - --secondary: 222.2 47.4% 11.2%; - --secondary-foreground: 210 40% 98%; - --destructive: 0 63% 31%; - --destructive-foreground: 210 40% 98%; - --ring: 216 34% 17%; - } + :root { + --background: 0 0% 100%; + --foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 47.4% 11.2%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --card: 0 0% 100%; + --card-foreground: 222.2 47.4% 11.2%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 100% 50%; + --destructive-foreground: 210 40% 98%; + --ring: 215 20.2% 65.1%; + --radius: 0.5rem; } - - @layer base { - * { - @apply border-border; - } - body { - @apply font-sans antialiased bg-background text-foreground; - overflow: hidden; - position: fixed; - width: 100%; - } + + .dark { + --background: 224 71% 4%; + --foreground: 213 31% 91%; + --muted: 223 47% 11%; + --muted-foreground: 215.4 16.3% 56.9%; + --accent: 216 34% 17%; + --accent-foreground: 210 40% 98%; + --popover: 224 71% 4%; + --popover-foreground: 215 20.2% 65.1%; + --border: 216 34% 17%; + --input: 216 34% 17%; + --card: 224 71% 4%; + --card-foreground: 213 31% 91%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 1.2%; + --secondary: 222.2 47.4% 11.2%; + --secondary-foreground: 210 40% 98%; + --destructive: 0 63% 31%; + --destructive-foreground: 210 40% 98%; + --ring: 216 34% 17%; } +} - @layer utilities { - .no-scrollbar::-webkit-scrollbar { - display: none; /* Chrome, Safari */ - } - - .no-scrollbar { - -ms-overflow-style: none; /* IE, Edge */ - scrollbar-width: none; /* Firefox */ - } +@layer base { + * { + @apply border-border; } + body { + @apply font-sans antialiased bg-background text-foreground; + overflow: hidden; + position: fixed; + width: 100%; + } +} - @supports (-webkit-touch-callout: none) { - input, textarea, select { - font-size: 16px !important; - } +@layer utilities { + .no-scrollbar::-webkit-scrollbar { + display: none; /* Chrome, Safari */ } - + .no-scrollbar { + -ms-overflow-style: none; /* IE, Edge */ + scrollbar-width: none; /* Firefox */ + } +} + +@supports (-webkit-touch-callout: none) { + input, + textarea, + select { + font-size: 16px !important; + } +} .swiper-pagination-bullet { width: 5px; @@ -129,7 +129,6 @@ .swiper-pagination-bullet-active { width: 5px; height: 5px; - background-color: white; - transform: scale(1.3); + background-color: white; + transform: scale(1.3); } - diff --git a/src/components/UI/ActivityStatus.tsx b/src/components/UI/ActivityStatus.tsx index 453cc69..f96b38b 100644 --- a/src/components/UI/ActivityStatus.tsx +++ b/src/components/UI/ActivityStatus.tsx @@ -15,7 +15,9 @@ type ActivitySection = { } // Fade In & Move up -const FadeInSection: React.FC<{ children: React.ReactNode }> = ({ children }) => { +const FadeInSection: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { return ( = ({ year }) => { items: [ { award: '장려상', - subtitle: '2024 동양미래 EXPO' - } - ] + subtitle: '2024 동양미래 EXPO', + }, + ], }, { category: '외부 경진대회 / 전시회', @@ -45,8 +47,8 @@ const ActivityStatus: React.FC<{ year: string }> = ({ year }) => { { award: '동상', subtitle: '교육장비 개발 및 아이디어 경진대회', - } - ] + }, + ], }, { category: '교내 경진대회', @@ -54,15 +56,15 @@ const ActivityStatus: React.FC<{ year: string }> = ({ year }) => { { award: '최우수상', subtitle: '컴퓨터 공학부 경진대회', - } - ] + }, + ], }, { category: '세미나 실적', items: [ { title: '현직 백엔드 개발자 특강 - ', subtitle: '20명 대상' }, - { title: '웹 개발 세미나 - ', subtitle: '10명 대상' } - ] + { title: '웹 개발 세미나 - ', subtitle: '10명 대상' }, + ], }, { category: '기타 활동', @@ -72,9 +74,9 @@ const ActivityStatus: React.FC<{ year: string }> = ({ year }) => { { title: 'DASOM MAKERS 스터디 및 홈페이지 제작' }, { title: '시험기간 간식 행사' }, { title: '할로윈 행사' }, - { title: '동계, 하계 MT' } - ] - } + { title: '동계, 하계 MT' }, + ], + }, ] return ( @@ -84,12 +86,18 @@ const ActivityStatus: React.FC<{ year: string }> = ({ year }) => { Dasom Icon
활동 현황
-
{year}
+
+ {year} +
-
- Activitybar -
+
+ Activitybar +
{activityData.map((section, index) => (
@@ -98,15 +106,25 @@ const ActivityStatus: React.FC<{ year: string }> = ({ year }) => {
    {section.items.map((activity, idx) => ( -
  • +
  • {activity.title && ( - {activity.title} + + {activity.title} + )} {activity.award && ( - {activity.award} + + {activity.award} + )} {activity.subtitle && ( - {' '}{activity.subtitle} + + {' '} + {activity.subtitle} + )}
  • ))} @@ -121,4 +139,4 @@ const ActivityStatus: React.FC<{ year: string }> = ({ year }) => { ) } -export default ActivityStatus \ No newline at end of file +export default ActivityStatus diff --git a/src/components/UI/AdminPagination.tsx b/src/components/UI/AdminPagination.tsx index 9a2faa6..1c91d01 100644 --- a/src/components/UI/AdminPagination.tsx +++ b/src/components/UI/AdminPagination.tsx @@ -1,48 +1,63 @@ import React from 'react' interface PaginationProps { - currentPage: number - totalPages: number - setCurrentPage: React.Dispatch> - setPage: React.Dispatch> + currentPage: number + totalPages: number + setCurrentPage: React.Dispatch> + setPage: React.Dispatch> } -const handlePrevPage = (setCurrentPage: React.Dispatch>, setPage: React.Dispatch>, totalPages: number) => { - setCurrentPage((prev) => { - const prevPage = Math.max(prev - 1, 1) - setPage(prevPage - 1) - return prevPage - }) +const handlePrevPage = ( + setCurrentPage: React.Dispatch>, + setPage: React.Dispatch>, + totalPages: number +) => { + setCurrentPage(prev => { + const prevPage = Math.max(prev - 1, 1) + setPage(prevPage - 1) + return prevPage + }) } -const handleNextPage = (setCurrentPage: React.Dispatch>, setPage: React.Dispatch>, totalPages: number) => { - setCurrentPage((prev) => { - const nextPage = Math.min(prev + 1, totalPages) - setPage(nextPage - 1) - return nextPage - }) +const handleNextPage = ( + setCurrentPage: React.Dispatch>, + setPage: React.Dispatch>, + totalPages: number +) => { + setCurrentPage(prev => { + const nextPage = Math.min(prev + 1, totalPages) + setPage(nextPage - 1) + return nextPage + }) } -const AdminPagination: React.FC = ({ currentPage, totalPages, setCurrentPage, setPage }) => { - return ( -
    - - {currentPage} / {totalPages} - -
    - ) +const AdminPagination: React.FC = ({ + currentPage, + totalPages, + setCurrentPage, + setPage, +}) => { + return ( +
    + + + {currentPage} / {totalPages} + + +
    + ) } -export default AdminPagination \ No newline at end of file +export default AdminPagination diff --git a/src/components/UI/FAQ_Section.tsx b/src/components/UI/FAQ_Section.tsx index 1fe84b1..86228f6 100644 --- a/src/components/UI/FAQ_Section.tsx +++ b/src/components/UI/FAQ_Section.tsx @@ -21,8 +21,12 @@ const FAQItem: React.FC = ({ question, answer, alignment }) => { variants={itemVariants} whileHover={{ scale: 1.03 }} > -

    {question}

    -

    {answer}

    +

    + {question} +

    +

    + {answer} +

    ) } @@ -34,42 +38,42 @@ const containerVariants = { transition: { staggerChildren: 0.1, ease: 'easeOut', - } - } + }, + }, } const FAQSection: React.FC = () => { return ( {/* FAQ 리스트 */} -
    +
    { ) } -export default FAQSection \ No newline at end of file +export default FAQSection diff --git a/src/components/UI/Header.tsx b/src/components/UI/Header.tsx index c7eb173..5f1b002 100644 --- a/src/components/UI/Header.tsx +++ b/src/components/UI/Header.tsx @@ -12,18 +12,24 @@ export const Header = (): JSX.Element => { const alertShown = useRef(false) const toggleMenu = () => { - setIsMenuOpen((prev) => !prev) + setIsMenuOpen(prev => !prev) } // 모집 기간 확인 useEffect(() => { const checkRecruitmentPeriod = async () => { try { - const response = await axios.get('https://dmu-dasom-api.or.kr/api/recruit') + const response = await axios.get( + 'https://dmu-dasom-api.or.kr/api/recruit' + ) const data = response.data - const recruitmentStart = data.find((item: any) => item.key === 'RECRUITMENT_PERIOD_START')?.value - const recruitmentEnd = data.find((item: any) => item.key === 'RECRUITMENT_PERIOD_END')?.value + const recruitmentStart = data.find( + (item: any) => item.key === 'RECRUITMENT_PERIOD_START' + )?.value + const recruitmentEnd = data.find( + (item: any) => item.key === 'RECRUITMENT_PERIOD_END' + )?.value const startDate = new Date(recruitmentStart) const endDate = new Date(recruitmentEnd) @@ -67,10 +73,13 @@ export const Header = (): JSX.Element => { margin: '0 auto', }} > -
    { - navigate('/') - isMenuOpen ? toggleMenu() : null - }}> +
    { + navigate('/') + isMenuOpen ? toggleMenu() : null + }} + > DASOM
    @@ -146,4 +155,4 @@ export const Header = (): JSX.Element => { )} ) -} \ No newline at end of file +} diff --git a/src/components/UI/MailButtons.tsx b/src/components/UI/MailButtons.tsx index 6161a04..059e45e 100644 --- a/src/components/UI/MailButtons.tsx +++ b/src/components/UI/MailButtons.tsx @@ -2,41 +2,49 @@ import React from 'react' import axios from 'axios' import { toast } from 'react-toastify' -const sendResultMail = async (mailType : any) => { - const confirmation = window.confirm(`${mailType === 'DOCUMENT_RESULT' ? '서류' : '면접'} 합격자에게 메일을 보낼까요?`) - if (!confirmation) return - const accessToken = localStorage.getItem('accessToken') +const sendResultMail = async (mailType: any) => { + const confirmation = window.confirm( + `${mailType === 'DOCUMENT_RESULT' ? '서류' : '면접'} 합격자에게 메일을 보낼까요?` + ) + if (!confirmation) return + const accessToken = localStorage.getItem('accessToken') - try { - await axios.post('https://dmu-dasom-api.or.kr/api/admin/applicants/send-email', { mailType }, { - headers: { - Authorization: `Bearer ${accessToken}` - } - }) - toast.success(`${mailType === 'DOCUMENT_RESULT' ? '서류' : '면접'} 합격자에게 메일을 발송했습니다.`) - } catch (error) { - toast.error('메일 발송에 실패했습니다.') - console.error('메일 발송 오류:', error) - } + try { + await axios.post( + 'https://dmu-dasom-api.or.kr/api/admin/applicants/send-email', + { mailType }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + toast.success( + `${mailType === 'DOCUMENT_RESULT' ? '서류' : '면접'} 합격자에게 메일을 발송했습니다.` + ) + } catch (error) { + toast.error('메일 발송에 실패했습니다.') + console.error('메일 발송 오류:', error) + } } const MailButtons = () => { - return ( -
    - - -
    - ) + return ( +
    + + +
    + ) } -export default MailButtons \ No newline at end of file +export default MailButtons diff --git a/src/components/UI/MeetingDate.tsx b/src/components/UI/MeetingDate.tsx index 614e46d..74b4094 100644 --- a/src/components/UI/MeetingDate.tsx +++ b/src/components/UI/MeetingDate.tsx @@ -1,26 +1,37 @@ import React, { JSX } from 'react' interface props { - date: string - onClick?: () => void - isSelected?: boolean // 선택 여부 + date: string + onClick?: () => void + isSelected?: boolean // 선택 여부 } -const getDisplatyDate = (date:string) => { - const d = new Date(date) - const month = (d.getMonth() + 1).toString() - const day = d.getDate().toString() - const week = ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'][d.getDay()] - return `${month}.${day} ${week}` +const getDisplatyDate = (date: string) => { + const d = new Date(date) + const month = (d.getMonth() + 1).toString() + const day = d.getDate().toString() + const week = [ + '일요일', + '월요일', + '화요일', + '수요일', + '목요일', + '금요일', + '토요일', + ][d.getDay()] + return `${month}.${day} ${week}` } -// 면접 날짜 선택 여부 컴포넌트 +// 면접 날짜 선택 여부 컴포넌트 const MeetingDate = ({ date, onClick, isSelected }: props): JSX.Element => { - return ( - - ) + return ( + + ) } -export default MeetingDate \ No newline at end of file +export default MeetingDate diff --git a/src/components/UI/MeetingDateSelector.tsx b/src/components/UI/MeetingDateSelector.tsx index cdf401e..bbacc5c 100644 --- a/src/components/UI/MeetingDateSelector.tsx +++ b/src/components/UI/MeetingDateSelector.tsx @@ -2,68 +2,75 @@ import React, { JSX, useEffect, useState } from 'react' import MeetingDate from './MeetingDate' interface interviewPeriod { - periodStart: string - periodEnd: string + periodStart: string + periodEnd: string } interface props { - onSelect: (date: string) => void - period: interviewPeriod // 면접 기간 + onSelect: (date: string) => void + period: interviewPeriod // 면접 기간 } -// 면접 날짜 선택 레이아웃 +// 면접 날짜 선택 레이아웃 const MeetingDateSelector = ({ onSelect, period }: props): JSX.Element => { - const [selectedDate, setSelectedDate] = useState('') - const [meetingDates, setMeetingDates] = useState([]) - - // 날짜 사이에 일 수 계산 - const getDateDiff = (d1: string, d2: string) => { - const date1 = new Date(d1) - const date2 = new Date(d2) + const [selectedDate, setSelectedDate] = useState('') + const [meetingDates, setMeetingDates] = useState([]) - const diffDate = date1.getTime() - date2.getTime() + // 날짜 사이에 일 수 계산 + const getDateDiff = (d1: string, d2: string) => { + const date1 = new Date(d1) + const date2 = new Date(d2) - return Math.abs(diffDate / (1000 * 60 * 60 * 24)) - } - - const getFormattedDate = (date: Date) => { - return date.toISOString().split('T')[0] - } + const diffDate = date1.getTime() - date2.getTime() - // 면접 예약 일자 목록 가져오기 - useEffect(() => { - const startDate = new Date(period.periodStart) - const dateArray: string[] = [] + return Math.abs(diffDate / (1000 * 60 * 60 * 24)) + } - for (let i = 0; i <= getDateDiff(period.periodStart, period.periodEnd); i++) { - const date = new Date(startDate) - date.setDate(startDate.getDate() + i) + const getFormattedDate = (date: Date) => { + return date.toISOString().split('T')[0] + } - dateArray.push(getFormattedDate(date)) - /* meetingDates[ + // 면접 예약 일자 목록 가져오기 + useEffect(() => { + const startDate = new Date(period.periodStart) + const dateArray: string[] = [] + + for ( + let i = 0; + i <= getDateDiff(period.periodStart, period.periodEnd); + i++ + ) { + const date = new Date(startDate) + date.setDate(startDate.getDate() + i) + + dateArray.push(getFormattedDate(date)) + /* meetingDates[ {date: '2025-03-12'}, ... ] */ - } - setMeetingDates(dateArray) - }, [period]) - - + } + setMeetingDates(dateArray) + }, [period]) - // 날짜 클릭 핸들러 (상태값 반환용) - const handleDateClick = (date: string) => { - setSelectedDate(date) - onSelect(date) - } + // 날짜 클릭 핸들러 (상태값 반환용) + const handleDateClick = (date: string) => { + setSelectedDate(date) + onSelect(date) + } - return ( -
    - {meetingDates.map((date, index) => ( - handleDateClick(date)} isSelected={selectedDate === date} date={date} /> - ))} -
    - ) + return ( +
    + {meetingDates.map((date, index) => ( + handleDateClick(date)} + isSelected={selectedDate === date} + date={date} + /> + ))} +
    + ) } -export default MeetingDateSelector \ No newline at end of file +export default MeetingDateSelector diff --git a/src/components/UI/MeetingTime.tsx b/src/components/UI/MeetingTime.tsx index 32b2df6..0f56cd5 100644 --- a/src/components/UI/MeetingTime.tsx +++ b/src/components/UI/MeetingTime.tsx @@ -1,24 +1,32 @@ import React, { JSX } from 'react' interface props { - time: string - onClick?: () => void // 시간 선택 핸들러 - isSelected?: boolean - disabled: boolean + time: string + onClick?: () => void // 시간 선택 핸들러 + isSelected?: boolean + disabled: boolean } // 면접 시간 선택 버튼 컴포넌트 -const MeetingTime = ({ time, onClick, isSelected, disabled }: props): JSX.Element => { - return ( - - ) +const MeetingTime = ({ + time, + onClick, + isSelected, + disabled, +}: props): JSX.Element => { + return ( + + ) } -export default MeetingTime \ No newline at end of file +export default MeetingTime diff --git a/src/components/UI/MeetingTimeSelector.tsx b/src/components/UI/MeetingTimeSelector.tsx index fc6923c..31ef1f6 100644 --- a/src/components/UI/MeetingTimeSelector.tsx +++ b/src/components/UI/MeetingTimeSelector.tsx @@ -2,82 +2,89 @@ import React, { JSX, useEffect, useState } from 'react' import MeetingTime from './MeetingTime' interface timeInfo { - time: string + time: string } interface interviewTime { - timeStart: string - timeEnd: string + timeStart: string + timeEnd: string } interface props { - onSelect: (time: string) => void - time: interviewTime // 면접 시간 - disabledSelectTime: boolean - slotsForDate: any[] + onSelect: (time: string) => void + time: interviewTime // 면접 시간 + disabledSelectTime: boolean + slotsForDate: any[] } -const MeetingTimeSelector = ({ onSelect, time, disabledSelectTime, slotsForDate }: props): JSX.Element => { - const [selectedTime, setSelectedTime] = useState('') - const [meetingTimes, setMeetingTimes] = useState([]) +const MeetingTimeSelector = ({ + onSelect, + time, + disabledSelectTime, + slotsForDate, +}: props): JSX.Element => { + const [selectedTime, setSelectedTime] = useState('') + const [meetingTimes, setMeetingTimes] = useState([]) - // 면접 예약 시간 목록 가져오기 - useEffect(() => { - //time -> 분으로 변환 - const parseTime = (timeStr: string): number => { - const [hours, minutes] = timeStr.split(':').map(Number) - return hours * 60 + minutes - } - // 분(totalMinutes)을 08:00 형식으로 변환 - const formatTime = (totalMinutes: number): string => { - const hours = Math.floor(totalMinutes / 60) - const minutes = totalMinutes % 60 - return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}` - } + // 면접 예약 시간 목록 가져오기 + useEffect(() => { + //time -> 분으로 변환 + const parseTime = (timeStr: string): number => { + const [hours, minutes] = timeStr.split(':').map(Number) + return hours * 60 + minutes + } + // 분(totalMinutes)을 08:00 형식으로 변환 + const formatTime = (totalMinutes: number): string => { + const hours = Math.floor(totalMinutes / 60) + const minutes = totalMinutes % 60 + return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}` + } - const startMinutes = parseTime(time.timeStart) - const endMinutes = parseTime(time.timeEnd) - const timeArray: timeInfo[] = [] + const startMinutes = parseTime(time.timeStart) + const endMinutes = parseTime(time.timeEnd) + const timeArray: timeInfo[] = [] - // endMinutes 이전까지 20분 간격으로 time 추가 - for (let minutes = startMinutes; minutes < endMinutes; minutes += 20) { - timeArray.push({ time: formatTime(minutes) }) - /* + // endMinutes 이전까지 20분 간격으로 time 추가 + for (let minutes = startMinutes; minutes < endMinutes; minutes += 20) { + timeArray.push({ time: formatTime(minutes) }) + /* meetingTimes[ {time: '12:00'}, {time: '12:20'}, ... ] */ - } + } - setMeetingTimes(timeArray) - }, [time]) + setMeetingTimes(timeArray) + }, [time]) - // 시간 클릭 핸들러 (부모컴포넌트에 state값 반환) - const handleTimeClick = (meetingTime: timeInfo) => { - setSelectedTime(meetingTime.time) - onSelect(meetingTime.time) - } + // 시간 클릭 핸들러 (부모컴포넌트에 state값 반환) + const handleTimeClick = (meetingTime: timeInfo) => { + setSelectedTime(meetingTime.time) + onSelect(meetingTime.time) + } - return ( -
    - {meetingTimes.map((meetingTime, index) => { - const matchingSlot = slotsForDate.find(slot => slot.startTime.startsWith(meetingTime.time)) - const isActive = matchingSlot?.interviewStatus === 'ACTIVE' + return ( +
    + {meetingTimes.map((meetingTime, index) => { + const matchingSlot = slotsForDate.find(slot => + slot.startTime.startsWith(meetingTime.time) + ) + const isActive = matchingSlot?.interviewStatus === 'ACTIVE' - return ( - handleTimeClick(meetingTime)} - isSelected={selectedTime === `${meetingTime.time}`} - disabled={!isActive || disabledSelectTime} - /> - ) - })} -
    - ) + return ( + handleTimeClick(meetingTime)} + isSelected={selectedTime === `${meetingTime.time}`} + disabled={!isActive || disabledSelectTime} + /> + ) + })} +
    + ) } -export default MeetingTimeSelector \ No newline at end of file +export default MeetingTimeSelector diff --git a/src/components/UI/NewsContent.tsx b/src/components/UI/NewsContent.tsx index 9694a39..e8ad221 100644 --- a/src/components/UI/NewsContent.tsx +++ b/src/components/UI/NewsContent.tsx @@ -5,80 +5,88 @@ import 'swiper/css' import 'swiper/css/pagination' interface NewsProps { - id: number - title: string - image?: string | null - images?: { encodedData: string; fileFormat: string }[] | null - createdAt: string - onClick: () => void - isDetail?: boolean + id: number + title: string + image?: string | null + images?: { encodedData: string; fileFormat: string }[] | null + createdAt: string + onClick: () => void + isDetail?: boolean } const NewsContent: React.FC = React.memo( - ({ id, title, image, images, createdAt, onClick, isDetail = false }) => { - const [loading, setLoading] = useState(true) + ({ id, title, image, images, createdAt, onClick, isDetail = false }) => { + const [loading, setLoading] = useState(true) - const formattedDate = useMemo( - () => new Date(createdAt).toISOString().split('T')[0].replace(/-/g, '.'), - [createdAt] - ) + const formattedDate = useMemo( + () => new Date(createdAt).toISOString().split('T')[0].replace(/-/g, '.'), + [createdAt] + ) - // Base64 → 이미지 URL 변환 (useMemo로 캐싱) - const imageUrls = useMemo(() => { - if (!images || images.length === 0) return null - return images.map((img) => - img?.encodedData ? `data:${img.fileFormat};base64,${img.encodedData}` : null - ) - }, [images]) + // Base64 → 이미지 URL 변환 (useMemo로 캐싱) + const imageUrls = useMemo(() => { + if (!images || images.length === 0) return null + return images.map(img => + img?.encodedData + ? `data:${img.fileFormat};base64,${img.encodedData}` + : null + ) + }, [images]) - return ( -
    - {loading && ( -
    - )} + return ( +
    + {loading && ( +
    + )} - {isDetail && imageUrls ? ( - - {imageUrls.map((imageUrl, index) => - imageUrl && ( - - {`뉴스 setLoading(false)} - loading='lazy' - /> - - ) - )} - - ) : ( - image && ( - 뉴스 대표 이미지 setLoading(false)} - loading='lazy' - /> - ) - )} -

    {title}

    -

    작성일: {formattedDate}

    -
    - ) - } + {isDetail && imageUrls ? ( + + {imageUrls.map( + (imageUrl, index) => + imageUrl && ( + + {`뉴스 setLoading(false)} + loading='lazy' + /> + + ) + )} + + ) : ( + image && ( + 뉴스 대표 이미지 setLoading(false)} + loading='lazy' + /> + ) + )} +

    + {title} +

    +

    작성일: {formattedDate}

    +
    + ) + } ) -export default NewsContent \ No newline at end of file +export default NewsContent diff --git a/src/components/UI/NewsFileUpload.tsx b/src/components/UI/NewsFileUpload.tsx index 9eeace5..687c29d 100644 --- a/src/components/UI/NewsFileUpload.tsx +++ b/src/components/UI/NewsFileUpload.tsx @@ -1,46 +1,50 @@ import React from 'react' interface FileUploadProps { - files: File[] - setFiles: React.Dispatch> + files: File[] + setFiles: React.Dispatch> } const NewsFileUpload: React.FC = ({ files, setFiles }) => { - const handleImageChange = (e: React.ChangeEvent) => { - if (e.target.files) { - const selectedFiles = Array.from(e.target.files) - setFiles((prevFiles) => [...prevFiles, ...selectedFiles]) - } + const handleImageChange = (e: React.ChangeEvent) => { + if (e.target.files) { + const selectedFiles = Array.from(e.target.files) + setFiles(prevFiles => [...prevFiles, ...selectedFiles]) } + } - const handleFileRemove = (index: number) => { - setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index)) - } + const handleFileRemove = (index: number) => { + setFiles(prevFiles => prevFiles.filter((_, i) => i !== index)) + } - return ( -
    - - + return ( +
    + + -
    -
    업로드 된 파일 목록
    - {Array.isArray(files) && files.length > 0 ? ( - files.map((file, index) => ( -
    handleFileRemove(index)}> - {file.name} -
    - )) - ) : ( -
    새로 업로드된 파일이 없습니다.
    - )} +
    +
    업로드 된 파일 목록
    + {Array.isArray(files) && files.length > 0 ? ( + files.map((file, index) => ( +
    handleFileRemove(index)} + > + {file.name}
    -
    - ) + )) + ) : ( +
    새로 업로드된 파일이 없습니다.
    + )} +
    +
    + ) } -export default NewsFileUpload \ No newline at end of file +export default NewsFileUpload diff --git a/src/components/UI/NewsNotice.tsx b/src/components/UI/NewsNotice.tsx index 052cd9d..41876bf 100644 --- a/src/components/UI/NewsNotice.tsx +++ b/src/components/UI/NewsNotice.tsx @@ -1,15 +1,18 @@ import React, { JSX } from 'react' interface notice { - text: string + text: string } const NewsNotice = ({ text }: notice): JSX.Element => { - return ( -
    -
    -
    - ) + return ( +
    +
    +
    + ) } -export default NewsNotice \ No newline at end of file +export default NewsNotice diff --git a/src/components/UI/NewsTextEditor.tsx b/src/components/UI/NewsTextEditor.tsx index a11765c..3593c7b 100644 --- a/src/components/UI/NewsTextEditor.tsx +++ b/src/components/UI/NewsTextEditor.tsx @@ -1,67 +1,81 @@ import React from 'react' interface TextEditorProps { - title: string - content: string - setTitle: React.Dispatch> - setContent: React.Dispatch> + title: string + content: string + setTitle: React.Dispatch> + setContent: React.Dispatch> } -const NewsTextEditor: React.FC = ({ title, content, setTitle, setContent }) => { - const autoResizeTextarea = (e: React.FormEvent) => { - const textarea = e.target as HTMLTextAreaElement - textarea.style.height = 'auto' - textarea.style.height = `${textarea.scrollHeight}px` - } +const NewsTextEditor: React.FC = ({ + title, + content, + setTitle, + setContent, +}) => { + const autoResizeTextarea = (e: React.FormEvent) => { + const textarea = e.target as HTMLTextAreaElement + textarea.style.height = 'auto' + textarea.style.height = `${textarea.scrollHeight}px` + } - const applyFormat = (tag: string, style?: string) => { - const formattedText = style - ? `` - : `<${tag}>` + const applyFormat = (tag: string, style?: string) => { + const formattedText = style + ? `` + : `<${tag}>` - setContent((prev) => prev + formattedText) - } + setContent(prev => prev + formattedText) + } - return ( -
    + return ( +
    + +