@@ -20,7 +20,7 @@ import {
2020 X ,
2121} from 'lucide-react'
2222import { listNotices } from './services/noticeApi.js'
23- import { listClubActivities } from './services/clubActivityApi.js'
23+ import { createClubActivity , listClubActivities } from './services/clubActivityApi.js'
2424import { getNotificationSummary , listNotifications , markAllNotificationsRead , markNotificationRead } from './services/notificationApi.js'
2525import { listFonts } from './services/fontApi.js'
2626import { BUILT_IN_FONTS , buildFontFaceCss , fontFamilyValue , injectBuiltinFontStylesheets } from './services/fontPreferences.js'
@@ -111,6 +111,18 @@ const showcaseItems = [
111111]
112112
113113const activityHubItems = [
114+ {
115+ title : 'Activity log' ,
116+ body : '관리자가 등록한 실제 활동 기록과 사진을 회원에게 보여줍니다.' ,
117+ route : '/activities' ,
118+ cta : '활동 기록 보기' ,
119+ } ,
120+ {
121+ title : 'Monthly calendar' ,
122+ body : '정기 회의, 세미나, 발표, 모집 마감을 월별 일정으로 확인합니다.' ,
123+ route : '/activities' ,
124+ cta : '일정 보기' ,
125+ } ,
114126 {
115127 title : '공지사항' ,
116128 body : '모집, 세미나, 운영 안내를 빠르게 확인합니다.' ,
@@ -390,6 +402,18 @@ function categoryLabel(value) {
390402 return labels [ value ] || value || '일반'
391403}
392404
405+ const clubActivityCategories = [
406+ [ 'GENERAL' , '일반' ] ,
407+ [ 'SEMINAR' , '세미나' ] ,
408+ [ 'STUDY' , '스터디' ] ,
409+ [ 'PROJECT' , '프로젝트' ] ,
410+ [ 'MEETING' , '회의' ] ,
411+ [ 'RECRUIT' , '모집' ] ,
412+ [ 'EVENT' , '행사' ] ,
413+ [ 'MT' , 'MT' ] ,
414+ [ 'ACHIEVEMENT' , '성과' ] ,
415+ ]
416+
393417const projectsDetailCards = [
394418 {
395419 title : 'Official Website' ,
@@ -1619,6 +1643,14 @@ function ClubCalendarSection({ compact = false }) {
16191643 const navigate = useNavigate ( )
16201644 const [ records , setRecords ] = useState ( null )
16211645 const [ error , setError ] = useState ( '' )
1646+ const [ scheduleForm , setScheduleForm ] = useState ( {
1647+ title : '' ,
1648+ eventDate : '' ,
1649+ category : 'MEETING' ,
1650+ description : '' ,
1651+ } )
1652+ const [ savingSchedule , setSavingSchedule ] = useState ( false )
1653+ const [ scheduleNotice , setScheduleNotice ] = useState ( '' )
16221654
16231655 useEffect ( ( ) => {
16241656 if ( authLoading || ! user ) {
@@ -1651,6 +1683,31 @@ function ClubCalendarSection({ compact = false }) {
16511683 return acc
16521684 } , { } )
16531685 const isLocked = ! authLoading && ! user
1686+ const isAdmin = user ?. role === 'ADMIN'
1687+
1688+ const submitSchedule = async ( event ) => {
1689+ event . preventDefault ( )
1690+ if ( ! scheduleForm . title . trim ( ) || ! scheduleForm . eventDate ) return
1691+ setSavingSchedule ( true )
1692+ setScheduleNotice ( '' )
1693+ setError ( '' )
1694+ try {
1695+ const created = await createClubActivity ( {
1696+ kind : 'SCHEDULE' ,
1697+ category : scheduleForm . category ,
1698+ title : scheduleForm . title . trim ( ) ,
1699+ description : scheduleForm . description . trim ( ) ,
1700+ eventDate : scheduleForm . eventDate ,
1701+ } )
1702+ setRecords ( ( prev ) => [ created , ...( Array . isArray ( prev ) ? prev : [ ] ) ] )
1703+ setScheduleNotice ( '일정을 추가했습니다.' )
1704+ setScheduleForm ( ( prev ) => ( { ...prev , title : '' , description : '' , eventDate : '' } ) )
1705+ } catch ( err ) {
1706+ setError ( err . message || '일정을 추가하지 못했습니다.' )
1707+ } finally {
1708+ setSavingSchedule ( false )
1709+ }
1710+ }
16541711
16551712 return (
16561713 < section className = { `club-calendar-section ${ compact ? 'club-calendar-section-compact' : '' } bg-[#f5f5f7] px-5 py-12 sm:py-16` } >
@@ -1669,6 +1726,57 @@ function ClubCalendarSection({ compact = false }) {
16691726 </ div >
16701727 </ div >
16711728
1729+ { isAdmin && ! isLocked && (
1730+ < form onSubmit = { submitSchedule } className = "calendar-admin-composer mt-8" aria-label = "캘린더 일정 추가" >
1731+ < div >
1732+ < p className = "calendar-admin-composer-title" > 관리자 일정 추가</ p >
1733+ < p className = "calendar-admin-composer-copy" > 캘린더에 바로 표시할 정기 회의, 세미나, 발표, 모집 마감 일정을 등록합니다.</ p >
1734+ </ div >
1735+ < label >
1736+ < span > 일정 제목</ span >
1737+ < input
1738+ value = { scheduleForm . title }
1739+ onChange = { ( event ) => setScheduleForm ( ( prev ) => ( { ...prev , title : event . target . value } ) ) }
1740+ maxLength = { 120 }
1741+ />
1742+ </ label >
1743+ < label >
1744+ < span > 일정 날짜</ span >
1745+ < input
1746+ type = "date"
1747+ value = { scheduleForm . eventDate }
1748+ onChange = { ( event ) => setScheduleForm ( ( prev ) => ( { ...prev , eventDate : event . target . value } ) ) }
1749+ />
1750+ </ label >
1751+ < label >
1752+ < span > 일정 분류</ span >
1753+ < select
1754+ value = { scheduleForm . category }
1755+ onChange = { ( event ) => setScheduleForm ( ( prev ) => ( { ...prev , category : event . target . value } ) ) }
1756+ >
1757+ { clubActivityCategories . map ( ( [ value , label ] ) => (
1758+ < option key = { value } value = { value } > { label } </ option >
1759+ ) ) }
1760+ </ select >
1761+ </ label >
1762+ < label className = "calendar-admin-composer-wide" >
1763+ < span > 일정 설명</ span >
1764+ < input
1765+ value = { scheduleForm . description }
1766+ onChange = { ( event ) => setScheduleForm ( ( prev ) => ( { ...prev , description : event . target . value } ) ) }
1767+ maxLength = { 500 }
1768+ />
1769+ </ label >
1770+ < button
1771+ type = "submit"
1772+ disabled = { savingSchedule || ! scheduleForm . title . trim ( ) || ! scheduleForm . eventDate }
1773+ >
1774+ { savingSchedule ? '추가 중...' : '일정 추가' }
1775+ </ button >
1776+ { scheduleNotice && < p className = "calendar-admin-composer-notice" > { scheduleNotice } </ p > }
1777+ </ form >
1778+ ) }
1779+
16721780 < div className = "club-calendar-shell mt-8" >
16731781 < div className = "club-calendar-weekdays" aria-hidden = "true" >
16741782 { calendarWeekdays . map ( ( weekday ) => (
@@ -2195,7 +2303,7 @@ function HomeView() {
21952303 </ div >
21962304 < button type = "button" onClick = { ( ) => goPageTop ( '/notices' ) } className = { ghostActionBtnClass } > 최근 공지 보기</ button >
21972305 </ div >
2198- < div className = "mt-8 grid gap-3 lg :grid-cols-3 " >
2306+ < div className = "mt-8 grid gap-3 md :grid-cols-2 xl:grid-cols-5 " >
21992307 { activityHubItems . map ( ( item , index ) => (
22002308 < button
22012309 key = { item . title }
0 commit comments