Skip to content
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

| **분야** | **이름** | **포지션** |
| --- | --- | --- |
| PM | <a href="https://github.com/titeotty">최도현</a> | **프론트엔드 리드**, 프론트 인프라 구축 & 서버 연동 및 배포, 화면 UI 구현, <br> UI/UX, GUI 디자인, 백엔드 API 및 DB 구축 |
| PM | <a href="https://github.com/dohy-eon">최도현</a> | **프론트엔드 리드**, 프론트 인프라 구축 & 서버 연동 및 배포, 화면 UI 구현, <br> UI/UX, GUI 디자인, 백엔드 API 및 DB 구축 |
| 백엔드 | <a href="https://github.com/ysw789">유승완</a> | **백엔드 리드**, 백엔드 인프라 구축 & 서버 연동 및 배포, API 및 DB 구축 |
| 백엔드 | <a href="https://github.com/hodoon">윤도훈</a> | **백엔드**, API 및 DB 구축 |
| 프론트엔드 | <a href="https://github.com/sooh329">김수현</a> | **프론트엔드**, 화면 UI 구현, API 연동 |
Expand Down
6 changes: 6 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ import FAQ from './pages/FAQ'
import { RecruitSubmit } from './pages/RecruitSubmit'
import RecruitCheck from './pages/RecruitCheck'
import RecruitCheckFinal from './pages/RecruitCheckFinal'
import SomkathonRecruit from './pages/SomkathonRecruit'
import SomkathonSubmit from './pages/SomkathonSubmit'
import ProtectedRoute from './components/layout/ProtectRoute'
import SomkatonApplicants from './pages/admin/SomkatonApplicants'

function App() {
return (
Expand Down Expand Up @@ -59,6 +62,8 @@ function AppContent() {
<Route path='/recruit/check/final' element={<RecruitCheckFinal />} />
<Route path='/recruit/meeting' element={<RecruitMeeting />} />
<Route path='/recruit/meeting/submit' element={<RecruitSubmitMeeting />} />
<Route path='/somkathon' element={<SomkathonRecruit />} />
<Route path='/somkathon/submit' element={<SomkathonSubmit />} />

{/* 관리자 페이지 */}
<Route path='/admin' element={<ProtectedRoute><AdminMain /></ProtectedRoute>} />
Expand All @@ -69,6 +74,7 @@ function AppContent() {
<Route path='/admin/news/:no' element={<ProtectedRoute><ManNewsDetail /></ProtectedRoute>} />
<Route path='/admin/news/post' element={<ProtectedRoute><ManNewsPost /></ProtectedRoute>} />
<Route path='/admin/news/edit/:no' element={<ProtectedRoute><ManNewsEdit /></ProtectedRoute>} />
<Route path='/admin/somkathon' element={<ProtectedRoute><SomkatonApplicants/></ProtectedRoute>}/>
</Routes>
</>
)
Expand Down
32 changes: 9 additions & 23 deletions src/components/UI/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,29 +132,15 @@ export const Header = (): JSX.Element => {
>
FAQ
</li>

{/* 모집 기간에 따라 버튼 다르게 표시 */}
{isRecruiting ? (
<li
className='font-pretendardBlack text-white text-[20px] cursor-pointer hover:scale-110'
onClick={() => {
navigate('/recruit')
toggleMenu()
}}
>
34기 지원하기
</li>
) : (
<li
className='font-pretendardBlack text-white text-[20px] cursor-pointer hover:scale-110'
onClick={() => {
navigate('/recruit/result')
toggleMenu()
}}
>
34기 합격여부 확인하기
</li>
)}
<li
className='font-pretendardBlack text-white text-[20px] cursor-pointer hover:scale-110'
onClick={() => {
navigate('/somkathon')
toggleMenu()
}}
>
솜커톤 지원하기
</li>
</ul>
</div>
)}
Expand Down
34 changes: 34 additions & 0 deletions src/components/UI/RecruitUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,38 @@ export const RecruitUI_FINAL2: React.FC<RecruitUIProps> = ({ name }) => {
</p>
</div>
)
}

export const SomRecruitUI: React.FC = () => {
return(
<div className="text-white font-pretendardRegular flex flex-col text-[12px] items-start w-auto shadow-[0px_2px_3px_rgba(255,255,255,0.2)] bg-#17171B] gap-2 mx-2">
<p className="pl-2 pt-2">
<span className='font-pretendardBold'>솜커톤</span>에서 멋진 프로젝트를 만들어 주실 학우 여러분들을 모집합니다!
</p>
<div className="mt-2 pl-2 flex">
<p className="text-mainColor font-pretendardSemiBold">📅 모집 일정 :</p>
<span className="text-white pl-1">4월 11일 (금) ~ 4월 16일 (수)</span>
</div>

<div className="mt-2 pl-2 flex items-center">
<p className="text-mainColor font-pretendardSemiBold">📝 모집 대상 :</p>
<span className="text-white pl-1">25년도 1학기 솜커톤에 참가하는 학우 여러분</span>
</div>

<div className="mt-2 pl-2 flex items-center">
<p className="text-mainColor font-pretendardSemiBold">🌿 신청 조건 :</p>
<span className="text-white pl-1 ">컴퓨터공학부 학생</span>
</div>

<div className="mt-2 pl-2">
<p className="text-mainColor font-pretendardSemiBold inline">🍀 참가비 :</p>
<span className="text-white pl-1 inline">10,000원</span>
<p className="text-white mt-1">
참가비는 솜커톤 행사 운영 자금으로 사용됩니다.
납부 방법은 추후 안내드리겠습니다.
</p>
</div>
<p className="pl-2 mb-4 ">👀 의지가 있으며 교류를 중시하는 분을 기다립니다.</p>
</div>
)
}
4 changes: 2 additions & 2 deletions src/pages/CoreMembers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const profiles: Profile[] = [
{ id: 1, name: '윤도훈', roll: '회장', github_username: 'hodoon' },
{ id: 2, name: '공석', roll: '부회장', github_username: '' },
{ id: 3, name: '유승완', roll: '기술팀장', github_username: 'ysw789' },
{ id: 4, name: '최도현', roll: '학술팀장', github_username: 'titeotty' },
{ id: 4, name: '최도현', roll: '학술팀장', github_username: 'dohy-eon' },
{ id: 5, name: '공석', roll: '학술차장', github_username: '' },
{ id: 6, name: '김수현', roll: '홍보팀장', github_username: 'sooh329' },
{ id: 7, name: '임성환', roll: '서기', github_username: 'limtjdghks' },
Expand Down Expand Up @@ -127,4 +127,4 @@ const CoreMembers: React.FC = () => {
)
}

export default CoreMembers
export default CoreMembers
182 changes: 182 additions & 0 deletions src/pages/SomkathonRecruit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import React, { useState, useEffect, useRef } from 'react'
import axios from 'axios'
import MobileLayout from '../components/layout/MobileLayout'
import { useNavigate } from 'react-router-dom'
import { SomRecruitUI, RecruitHeader } from '../components/UI/RecruitUI'
import { InputField } from '../components/UI/Recruit_InputField'
import { Button } from '../components/UI/Recruit_Button'


const SomkathonRecruit: React.FC = () => {
const navigate = useNavigate()
const [contact, setContact] = useState('')
const [isRecruiting, setIsRecruiting] = useState<boolean | null>(null)
const alertShown = useRef(false)

const [formData, setFormData] = useState({
participantName: '',
studentId: '',
department: '',
grade: '1',
contact: '',
email: '',
})

useEffect(() => {
const checkRecruitmentPeriod = async () => {
const startDate = new Date('2025-04-08T00:00:00')
const endDate = new Date('2025-04-17T00:00:00')
const now = new Date()

if (now >= startDate && now <= endDate) {
setIsRecruiting(true)
} else {
setIsRecruiting(false)
if (!alertShown.current) {
alertShown.current = true
alert('현재 모집 기간이 아닙니다.')
navigate('/')
}
}
}

checkRecruitmentPeriod()
}, [navigate])

if (isRecruiting === false) return null

// 입력값들 제약조건 설정
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target
let newValue = value

if (name === 'contact') {
let formattedValue = value.replace(/[^0-9]/g, '')


if (formattedValue.length > 11) {
formattedValue = formattedValue.slice(0, 11)
}

if (formattedValue.length === 10) {
formattedValue = formattedValue.replace(/^(\d{3})(\d{3})(\d{4})$/, '$1-$2-$3')
} else if (formattedValue.length === 11) {
formattedValue = formattedValue.replace(/^(\d{3})(\d{4})(\d{4})$/, '$1-$2-$3')
}

setContact(formattedValue)
setFormData((prevData) => ({
...prevData,
contact: formattedValue
}))
}
else if (name === 'participantName') {
if (value.length <= 16) {
setFormData((prevData) => ({
...prevData,
participantName: value
}))
}
} else if (name === 'studentId') {
let formattedValue = value.replace(/[^0-9]/g, '')
formattedValue = formattedValue.slice(0, 8)

setFormData((prevData) => ({
...prevData,
studentId: formattedValue
}))
} else {
setFormData((prevData) => ({
...prevData,
[name]: newValue
}))
}
}

const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
if (e.key === 'Enter') {
e.preventDefault()

const form = e.currentTarget.form
if (!form) return

const elements = Array.from(form.elements) as HTMLElement[]
const index = elements.indexOf(e.currentTarget)


for (let i = index + 1; i < elements.length; i++) {
const nextElement = elements[i]
if (nextElement instanceof HTMLInputElement || nextElement instanceof HTMLTextAreaElement || nextElement instanceof HTMLSelectElement) {
nextElement.focus()
break
}
}
}
}

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!formData.participantName || !formData.studentId || !formData.contact || !formData.email || !formData.department) {
alert('모든 필수 정보를 입력해주세요.')
return
}

try {
await axios.post('https://dmu-dasom-api.or.kr/api/somkathon/participants/create', formData, {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})

navigate('/somkathon/submit')
} catch (error: any) {
if (error.response) {
const errorData = error.response.data

if (error.response.status === 400 && errorData.code === 'C001' || errorData.code === 'C007') {
alert('입력 정보를 다시 확인해주세요.')
} else if (error.response.status === 400 && errorData.code === 'C013'){
alert('이미 등록된 학번입니다.')
}
} else {
console.error('API 요청 중 오류 발생:', error)
alert('네트워크 오류가 발생했습니다.')
}
}
}

return (
<MobileLayout>
<RecruitHeader title='다솜 해커톤 프로젝트 : 솜커톤 SOMKATHON 모집 폼' />
<SomRecruitUI />
<div className='flex flex-col items-center gap-6 mb-40'>
<form className='mt-3 bg-mainBlack w-full px-2 font-pretendardRegular' onSubmit={handleSubmit} >
<InputField label='이름' name='participantName' value={formData.participantName} onChange={handleInputChange} onKeyDown={handleKeyPress}
required minLength={1} maxLength={16} />
<InputField label='학번' name='studentId' value={formData.studentId} onChange={handleInputChange} highlightLabels={[]} required />
<InputField label='학과' name='department' value={formData.department} onChange={handleInputChange} required />
<InputField label='연락처' name='contact' placeholder='숫자만 입력해주세요' value={formData.contact} onChange={handleInputChange} required />
<InputField label='이메일' name='email' type='email' value={formData.email} onChange={handleInputChange} required />
<InputField
label='학년'
name='grade'
type='select'
value={formData.grade}
onChange={handleInputChange}
required
options={[
{ value: '1', label: '1학년' },
{ value: '2', label: '2학년' },
{ value: '3', label: '3학년' },
{ value: '4', label: '4학년' }
]}
/>
<Button text='폼 제출하기' />
</form>
</div>
</MobileLayout>
)
}

export default SomkathonRecruit
21 changes: 21 additions & 0 deletions src/pages/SomkathonSubmit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react'
import MobileLayout from '../components/layout/MobileLayout'
import { Header } from '../components/UI/Header'
import { SomRecruitUI, RecruitHeader } from '../components/UI/RecruitUI'
import { Recruit_InfoBanner } from '../components/UI/Recruit_InfoBanner'

const SomkathonSubmit: React.FC = () => {
return(
<MobileLayout>
<Header />
<RecruitHeader title="다솜 해커톤 프로젝트 : 솜커톤 SOMKATHON 모집 폼" />
<SomRecruitUI />
<Recruit_InfoBanner
message={`솜커톤에 지원해주셔서 감사합니다.
모집 완료 후 카카오톡 단체 톡방으로 초대해드리겠습니다.`}
/>
</MobileLayout>
)
}

export default SomkathonSubmit
1 change: 1 addition & 0 deletions src/pages/admin/AdminMain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const AdminMain: React.FC = () => {
<AdminMenuBtn text='지원자 관리' link='/admin/applicants' />
<AdminMenuBtn text='일정 관리' link='/admin/date' />
<AdminMenuBtn text='공지사항 관리' link='/admin/news' />
<AdminMenuBtn text='솜커톤 지원자 관리' link='/admin/somkathon' />
<div
className='cursor-pointer text-[14px] font-pretendardRegular text-white'
onClick={handleLogout}
Expand Down
Loading