1- import React from 'react'
1+ import React , { useEffect , useState } from 'react'
22import { motion } from 'framer-motion'
33import DasomLogo from '../../assets/images/dasomLogo.svg'
4- import ActivityBar from '../../assets/images/activityBar.svg'
54import { ActivityStatusProps , ActivitySection } from './types'
5+ import axios from 'axios'
6+
7+ interface ApiActivity {
8+ id : number
9+ monthDay : string
10+ title : string
11+ award : string | null
12+ }
13+
14+ interface ApiSection {
15+ id : number
16+ section : string
17+ activities : ApiActivity [ ]
18+ }
19+
20+ interface ApiYearData {
21+ year : number
22+ sections : ApiSection [ ]
23+ }
624
7- // 페이드 인 및 위로 이동 애니메이션
825const FadeInSection : React . FC < { children : React . ReactNode } > = ( {
926 children,
10- } ) => {
11- return (
12- < motion . div
13- initial = { { opacity : 0 , y : 20 } }
14- whileInView = { { opacity : 1 , y : 0 } }
15- transition = { { duration : 0.8 , ease : 'easeOut' } }
16- viewport = { { once : false } }
17- >
18- { children }
19- </ motion . div >
20- )
21- }
27+ } ) => (
28+ < motion . div
29+ initial = { { opacity : 0 , y : 10 } }
30+ whileInView = { { opacity : 1 , y : 0 } }
31+ transition = { { duration : 0.6 , ease : 'easeOut' } }
32+ viewport = { { once : true , amount : 0.2 } }
33+ >
34+ { children }
35+ </ motion . div >
36+ )
2237
23- const ActivityStatus : React . FC < ActivityStatusProps > = ( {
24- year,
38+ const ActivityStatus : React . FC < ActivityStatusProps > = ( {
39+ year,
2540 activityData : customActivityData ,
26- className = ''
41+ className = '' ,
2742} ) => {
28- // 2024년도 기본 활동 데이터
29- const defaultActivityData2024 : ActivitySection [ ] = [
30- {
31- category : '코엑스 한국전자전' ,
32- items : [
33- {
34- award : '장려상' ,
35- subtitle : '2024 동양미래 EXPO' ,
36- } ,
37- ] ,
38- } ,
39- {
40- category : '외부 경진대회 / 전시회' ,
41- items : [
42- {
43- award : '동상' ,
44- subtitle : '교육장비 개발 및 아이디어 경진대회' ,
45- } ,
46- ] ,
47- } ,
48- {
49- category : '교내 경진대회' ,
50- items : [
51- {
52- award : '최우수상' ,
53- subtitle : '컴퓨터 공학부 경진대회' ,
54- } ,
55- ] ,
56- } ,
57- {
58- category : '세미나 실적' ,
59- items : [
60- { title : '현직 백엔드 개발자 특강 - ' , subtitle : '20명 대상' } ,
61- { title : '웹 개발 세미나 - ' , subtitle : '10명 대상' } ,
62- ] ,
63- } ,
64- {
65- category : '기타 활동' ,
66- items : [
67- { title : '컴퓨터공학부 최초 해커톤 개최' } ,
68- { title : '전공동아리 내부 팀 프로젝트 발표회 개최' } ,
69- { title : 'DASOM MAKERS 스터디 및 홈페이지 제작' } ,
70- { title : '시험기간 간식 행사' } ,
71- { title : '할로윈 행사' } ,
72- { title : '동계, 하계 MT' } ,
73- ] ,
74- } ,
75- ]
76-
77- // 2025년도 기본 활동 데이터
78- const defaultActivityData2025 : ActivitySection [ ] = [
79- {
80- category : '신규 프로젝트' ,
81- items : [
82- {
83- title : 'NFT 기반 타임캡슐 서비스 - ' ,
84- subtitle : ' 기획, 디자인 및 시연' ,
85- } ,
86- ] ,
87- } ,
88- {
89- category : '세미나 및 워크샵' ,
90- items : [
91- { title : '나의 커리어 디자인하기 - 나에게 맞는 회사 고민하기, 성장 전략은? ' , subtitle : ' ' } ,
92- ] ,
93- } ,
94- {
95- category : '대회 참가' ,
96- items : [
97- {
98- award : '장려상 ' ,
99- subtitle : '생성형 AI를 활용한 문제해결 해커톤' ,
100- } ,
101- ] ,
102- } ,
103- {
104- category : '2025년 기타 활동' ,
105- items : [
106- { title : '스프링 부트, 팀 프로젝트 기획 개발 스터디 그룹 운영' } ,
107- { title : '컴퓨터공학부 + 시각디자인학부 협업 해커톤 개최' } ,
108- { title : '대학생 IT 연합동아리 DND, UMC 활동' } ,
109- { title : '오픈소스 프로젝트 기여 활동' } ,
110- { title : '2025년 동계 MT 계획' } ,
111- { title : '미니퀴즈 간식행사' } ,
112- ] ,
113- } ,
114- ]
115-
116- // 커스텀 데이터가 있으면 사용하고, 없으면 연도에 따른 기본 데이터 사용
117- const data = customActivityData || ( year === '2025' ? defaultActivityData2025 : defaultActivityData2024 )
43+ const [ apiData , setApiData ] = useState < ActivitySection [ ] > ( [ ] )
44+ const [ loading , setLoading ] = useState ( true )
45+
46+ useEffect ( ( ) => {
47+ const fetchActivities = async ( ) => {
48+ try {
49+ setLoading ( true )
50+
51+ const response = await axios . get < ApiYearData [ ] > (
52+ 'https://api.dmu-dasom.or.kr/api/activities'
53+ )
54+
55+ const result = response . data
56+
57+ const currentYearData = result . find (
58+ item => item . year === Number ( year )
59+ )
60+
61+ if ( ! currentYearData ) {
62+ setApiData ( [ ] )
63+ return
64+ }
65+
66+ const transformed : ActivitySection [ ] =
67+ currentYearData . sections . map ( section => ( {
68+ category : section . section ,
69+ items : section . activities . map ( activity => ( {
70+ title : activity . title ,
71+ award : activity . award ?? undefined ,
72+ subtitle : activity . monthDay
73+ ? `(${ activity . monthDay } )`
74+ : undefined ,
75+ } ) ) ,
76+ } ) )
77+
78+ setApiData ( transformed )
79+ } catch ( error ) {
80+ console . error ( '활동 데이터 불러오기 실패:' , error )
81+ setApiData ( [ ] )
82+ } finally {
83+ setLoading ( false )
84+ }
85+ }
86+
87+ fetchActivities ( )
88+ } , [ year ] )
89+
90+ const defaultActivityData2024 : ActivitySection [ ] = [ ]
91+ const defaultActivityData2025 : ActivitySection [ ] = [ ]
92+
93+ const data =
94+ customActivityData ||
95+ ( apiData . length > 0
96+ ? apiData
97+ : year === '2025'
98+ ? defaultActivityData2025
99+ : defaultActivityData2024 )
100+
101+ if ( loading ) {
102+ return < div className = "text-white text-center py-10" > Loading...</ div >
103+ }
118104
119105 return (
120- < FadeInSection >
121- < div className = { `max-w-[400px] bg-mainBlack p-4 rounded-xl text-white ${ className } ` } >
122- < div className = 'flex items-center gap-2 mb-3' >
123- < img src = { DasomLogo } className = 'w-7 h-7' alt = 'Dasom Icon' />
106+ < FadeInSection key = { year } >
107+ < div
108+ className = { `max-w-[400px] bg-mainBlack p-4 rounded-xl text-white ${ className } ` }
109+ >
110+ { /* 헤더 */ }
111+ < div className = "flex items-center gap-2 mb-6" >
112+ < img src = { DasomLogo } className = "w-7 h-7" alt = "Dasom Icon" />
124113 < div >
125- < div className = 'text-[16px] font-pretendardBold' > 활동 현황</ div >
126- < div className = 'text-mainColor text-[13px] font-pretendardSemiBold' >
114+ < div className = "text-[16px] font-pretendardBold" >
115+ 활동 현황
116+ </ div >
117+ < div className = "text-mainColor text-[13px] font-pretendardSemiBold" >
127118 { year }
128119 </ div >
129120 </ div >
130121 </ div >
131- < div className = 'flex items-start gap-3' >
132- < img
133- src = { ActivityBar }
134- className = 'w-4 h-[300px] mt-1.5'
135- alt = 'Activitybar'
136- />
137- < div className = 'space-y-3' >
122+
123+ { /* 타임라인 */ }
124+ < div className = "relative" >
125+
126+ < div className = "space-y-6" >
138127 { data . map ( ( section , index ) => (
139128 < FadeInSection key = { index } >
140- < div >
141- < div className = 'text-white text-[12px] font-pretendardBold' >
142- { section . category }
129+ < div className = "flex gap-4 relative" >
130+
131+ { /* 점 + 선 영역 */ }
132+ < div className = "relative w-6 flex justify-center" >
133+
134+ { /* 세로 점선 (가운데 정렬) */ }
135+ < div
136+ className = "absolute top-0 bottom-0 left-1/2
137+ -translate-x-1/2
138+ border-l-2 border-dashed border-mainColor/40"
139+ />
140+
141+ { /* 점 */ }
142+ < span
143+ className = "relative z-10 w-1.5 h-1.5 rounded-full bg-mainColor
144+ shadow-[0_0_10px_rgba(0,255,200,0.8)]"
145+ />
146+ </ div >
147+
148+ { /* 콘텐츠 */ }
149+ < div className = "flex-1" >
150+ < div className = "text-white text-[12px] font-pretendardBold mb-1" >
151+ { section . category }
152+ </ div >
153+
154+ < ul className = "space-y-1" >
155+ { section . items . map ( ( activity , idx ) => (
156+ < li
157+ key = { idx }
158+ className = "flex flex-wrap text-[10.5px] leading-tight"
159+ >
160+ { activity . award && (
161+ < span className = "font-pretendardBold text-mainColor mr-1" >
162+ { activity . award }
163+ </ span >
164+ ) }
165+ { activity . title && (
166+ < span className = "font-pretendardRegular" >
167+ { activity . title }
168+ </ span >
169+ ) }
170+ { activity . subtitle && (
171+ < span className = "text-subGrey font-pretendardRegular ml-1" >
172+ { activity . subtitle }
173+ </ span >
174+ ) }
175+ </ li >
176+ ) ) }
177+ </ ul >
143178 </ div >
144- < ul className = 'space-y-1' >
145- { section . items . map ( ( activity , idx ) => (
146- < li
147- key = { idx }
148- className = 'flex flex-wrap text-[10.5px] leading-tight'
149- >
150- { activity . title && (
151- < span className = 'font-pretendardRegular' >
152- { activity . title }
153- </ span >
154- ) }
155- { activity . award && (
156- < span className = 'font-pretendardBold text-mainColor mr-1' >
157- { activity . award }
158- </ span >
159- ) }
160- { activity . subtitle && (
161- < span className = 'text-subGrey font-pretendardRegular' >
162- { ' ' }
163- { activity . subtitle }
164- </ span >
165- ) }
166- </ li >
167- ) ) }
168- </ ul >
179+
169180 </ div >
170181 </ FadeInSection >
171182 ) ) }
172183 </ div >
173184 </ div >
185+
174186 </ div >
175187 </ FadeInSection >
176188 )
177189}
178190
179- export default ActivityStatus
191+ export default ActivityStatus
0 commit comments