@@ -14,6 +14,7 @@ const editIcon = "/admin/img/icon/edit.svg";
1414const co2Icon = "/admin/img/icon/co2.svg" ;
1515const waterIcon = "/admin/img/icon/water.svg" ;
1616const energyIcon = "/admin/img/icon/energy.svg" ;
17+ const reportIcon = "/admin/img/icon/document.svg" ;
1718
1819interface EventImage {
1920 imageId : number ;
@@ -84,6 +85,13 @@ interface StaffCodeResponse {
8485 issuedAt : string ;
8586}
8687
88+ interface EventReportResponse {
89+ reportId : string ;
90+ status : string ;
91+ downloadUrl ?: string ;
92+ message ?: string ;
93+ }
94+
8795const EventDetail : React . FC = ( ) => {
8896 const { id } = useParams < { id : string } > ( ) ;
8997 const navigate = useNavigate ( ) ;
@@ -97,6 +105,7 @@ const EventDetail: React.FC = () => {
97105 const [ staffCodeError , setStaffCodeError ] = useState < string | null > ( null ) ;
98106 const [ showIssueModal , setShowIssueModal ] = useState < boolean > ( false ) ;
99107 const [ showReissueModal , setShowReissueModal ] = useState < boolean > ( false ) ;
108+ const [ isDownloadingReport , setIsDownloadingReport ] = useState < boolean > ( false ) ;
100109
101110 const sidebarRef = useRef < HTMLDivElement | null > ( null ) ;
102111 const sidebarInnerRef = useRef < HTMLDivElement | null > ( null ) ;
@@ -322,6 +331,61 @@ const EventDetail: React.FC = () => {
322331 }
323332 } ;
324333
334+ const buildDownloadUrl = ( rawUrl : string ) : string => {
335+ if ( ! rawUrl ) return "" ;
336+ const base = process . env . REACT_APP_API_BASE_URL || window . location . origin ;
337+ try {
338+ if ( rawUrl . startsWith ( "http" ) ) {
339+ return rawUrl ;
340+ }
341+ return new URL ( rawUrl , base ) . toString ( ) ;
342+ } catch ( error ) {
343+ console . error ( "보고서 다운로드 URL 변환 실패:" , error ) ;
344+ return rawUrl ;
345+ }
346+ } ;
347+
348+ const handleDownloadReport = async ( ) => {
349+ if ( ! id ) return ;
350+
351+ try {
352+ setIsDownloadingReport ( true ) ;
353+ const response = await apiRequest ( `/admin/reports?eventId=${ id } ` , {
354+ method : "POST" ,
355+ } ) ;
356+
357+ if ( ! response . ok ) {
358+ const errorData = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
359+ throw new Error ( errorData . message || "행사 보고서 다운로드에 실패했습니다." ) ;
360+ }
361+
362+ const data : EventReportResponse = await response . json ( ) ;
363+ const status = data . status ? data . status . toUpperCase ( ) : "" ;
364+
365+ if ( status === "FAILED" ) {
366+ throw new Error ( "보고서 집계가 완료되지 않았습니다. 잠시 후 다시 시도해주세요." ) ;
367+ }
368+
369+ if ( status === "READY" && data . downloadUrl ) {
370+ const downloadUrl = buildDownloadUrl ( data . downloadUrl ) ;
371+ const anchor = document . createElement ( "a" ) ;
372+ anchor . href = downloadUrl ;
373+ anchor . target = "_blank" ;
374+ anchor . rel = "noopener noreferrer" ;
375+ anchor . click ( ) ;
376+ return ;
377+ }
378+
379+ alert ( "보고서를 준비 중입니다. 잠시 후 다시 시도해주세요." ) ;
380+ } catch ( err ) {
381+ console . error ( "행사 보고서 다운로드 실패:" , err ) ;
382+ const message = err instanceof Error ? err . message : "행사 보고서 다운로드에 실패했습니다." ;
383+ alert ( message ) ;
384+ } finally {
385+ setIsDownloadingReport ( false ) ;
386+ }
387+ } ;
388+
325389 const calculateTotalCapacity = ( options : EventOption [ ] ) : number => {
326390 let total = 0 ;
327391 const traverse = ( opts : EventOption [ ] ) => {
@@ -373,6 +437,10 @@ const EventDetail: React.FC = () => {
373437 }
374438 } ;
375439
440+ const isEventCompleted = ( status : string ) : boolean => {
441+ return mapApiStatusToDisplayStatus ( status ) === "completed" ;
442+ } ;
443+
376444 const getStatusBadge = ( status : string ) => {
377445 const displayStatus = mapApiStatusToDisplayStatus ( status ) ;
378446 switch ( displayStatus ) {
@@ -512,6 +580,7 @@ const EventDetail: React.FC = () => {
512580 const totalCapacity = calculateTotalCapacity ( eventData . options ) ;
513581 const totalAppliedCount = calculateTotalAppliedCount ( eventData . options ) ;
514582 const totalRemainingCount = totalCapacity - totalAppliedCount ;
583+ const isCompletedEvent = isEventCompleted ( eventData . status ) ;
515584
516585 return (
517586 < div className = { styles [ "event-detail-page" ] } >
@@ -839,13 +908,25 @@ const EventDetail: React.FC = () => {
839908
840909 { /* 액션 버튼들 */ }
841910 < div style = { { display : "flex" , gap : "4px" , flexDirection : "column" } } >
842- < button
843- className = { styles [ "edit-button" ] }
844- onClick = { ( ) => navigate ( `/events/${ id } /edit` ) }
845- >
846- < img src = { editIcon } alt = "수정 아이콘" />
847- 수정하기
848- </ button >
911+ { isCompletedEvent && (
912+ < button
913+ className = { `${ styles [ "edit-button" ] } ${ styles [ "download-button" ] } ` }
914+ onClick = { handleDownloadReport }
915+ disabled = { isDownloadingReport }
916+ >
917+ < img src = { reportIcon } alt = "보고서 다운로드 아이콘" />
918+ { isDownloadingReport ? "다운로드 중..." : "보고서 다운로드" }
919+ </ button >
920+ ) }
921+ { ! isCompletedEvent && (
922+ < button
923+ className = { styles [ "edit-button" ] }
924+ onClick = { ( ) => navigate ( `/events/${ id } /edit` ) }
925+ >
926+ < img src = { editIcon } alt = "수정 아이콘" />
927+ 수정하기
928+ </ button >
929+ ) }
849930 < button
850931 className = { `${ styles [ "edit-button" ] } ${ styles [ "participants-button" ] } ` }
851932 onClick = { ( ) => navigate ( `/events/${ id } /participants` ) }
0 commit comments