@@ -14,6 +14,32 @@ const setQS = (entries) => {
1414 window . history . replaceState ( { } , '' , u . toString ( ) ) ;
1515} ;
1616
17+ // ===== CSV 저장 공통 헬퍼 =====
18+ const saveResponseAsFile = ( res , fallbackName ) => {
19+ // Excel 한글 깨짐 방지용 BOM
20+ const BOM = new Uint8Array ( [ 0xEF , 0xBB , 0xBF ] ) ;
21+ const blob = new Blob ( [ BOM , res . data ] , { type : 'text/csv;charset=utf-8' } ) ;
22+
23+ // 서버가 파일명 내려주면 우선 사용
24+ let filename = fallbackName ;
25+ const cd = res . headers && ( res . headers [ 'content-disposition' ] || res . headers [ 'Content-Disposition' ] ) ;
26+ if ( cd ) {
27+ const m = / f i l e n a m e \* = U T F - 8 ' ' ( [ ^ ; ] + ) | f i l e n a m e = " ? ( [ ^ " ] + ) " ? / i. exec ( cd ) ;
28+ const decoded = m && decodeURIComponent ( ( m [ 1 ] || m [ 2 ] || '' ) . trim ( ) ) ;
29+ if ( decoded ) filename = decoded ;
30+ }
31+
32+ const url = URL . createObjectURL ( blob ) ;
33+ const a = document . createElement ( 'a' ) ;
34+ a . href = url ;
35+ a . download = filename ;
36+ document . body . appendChild ( a ) ;
37+ a . click ( ) ;
38+ a . remove ( ) ;
39+ URL . revokeObjectURL ( url ) ;
40+ } ;
41+
42+
1743export default function AttendancePage ( ) {
1844 const { apiClient} = useAuthenticatedApi ( ) ;
1945
@@ -49,6 +75,17 @@ export default function AttendancePage() {
4975 } ) ) . data . data ,
5076
5177 summary : async ( d ) => ( await apiClient . get ( `/core-attendance/meetings/${ d } /summary` ) ) . data . data ,
78+
79+ downloadSummaryCsvForDateAndSave : async ( d ) => {
80+ const res = await apiClient . get ( `/core-attendance/meetings/${ d } /summary.csv` , { responseType : 'blob' } ) ;
81+ saveResponseAsFile ( res , `attendance-${ d } .csv` ) ;
82+ } ,
83+
84+ downloadSummaryCsvAllAndSave : async ( ) => {
85+ const res = await apiClient . get ( '/core-attendance/meetings/summary.csv' , { responseType : 'blob' } ) ;
86+ saveResponseAsFile ( res , 'attendance-summary.csv' ) ;
87+ } ,
88+
5289 } ;
5390
5491 /** URL 동기화 */
@@ -366,6 +403,25 @@ export default function AttendancePage() {
366403 < Card className = "bg-default-100 dark:bg-default-50" >
367404 < CardBody className = "gap-3 text-white" >
368405 < b > 요약</ b >
406+ < Button
407+ size = "sm"
408+ variant = "flat"
409+ color = "primary"
410+ onPress = { ( ) => api . downloadSummaryCsvForDateAndSave ( date ) }
411+ isDisabled = { ! date }
412+ >
413+ 날짜 CSV
414+ </ Button >
415+
416+ < Button
417+ size = "sm"
418+ variant = "flat"
419+ color = "secondary"
420+ onPress = { ( ) => api . downloadSummaryCsvAllAndSave ( ) }
421+ >
422+ 전체 CSV
423+ </ Button >
424+
369425 { summary ? ( < div className = "text-sm" >
370426 < div className = "mb-2" > 전체 { summary . present } / { summary . total } </ div >
371427 < Divider />
0 commit comments