@@ -30,6 +30,7 @@ export function AdminDashboardPage() {
3030 const [ newAssetDescription , setNewAssetDescription ] = useState ( '' ) ;
3131 const [ newAssetQuantity , setNewAssetQuantity ] = useState ( 1 ) ;
3232 const [ newAssetLocation , setNewAssetLocation ] = useState ( '' ) ;
33+ const [ newAssetMaxRentalDays , setNewAssetMaxRentalDays ] = useState < number | null > ( null ) ;
3334
3435 const [ isAddingAsset , setIsAddingAsset ] = useState ( false ) ;
3536 const [ addAssetError , setAddAssetError ] = useState < string | null > ( null ) ;
@@ -46,6 +47,7 @@ export function AdminDashboardPage() {
4647 description : string ;
4748 quantity : number ;
4849 location : string ;
50+ max_rental_days : number | null ;
4951 } | null > ( null ) ;
5052 const [ isUpdatingAsset , setIsUpdatingAsset ] = useState ( false ) ;
5153 const [ showEditModal , setShowEditModal ] = useState ( false ) ;
@@ -227,6 +229,7 @@ export function AdminDashboardPage() {
227229 setNewAssetDescription ( '' ) ;
228230 setNewAssetQuantity ( 1 ) ;
229231 setNewAssetLocation ( '' ) ;
232+ setNewAssetMaxRentalDays ( null ) ;
230233
231234 setAddAssetError ( null ) ;
232235 setShowAddAssetModal ( true ) ;
@@ -258,6 +261,7 @@ export function AdminDashboardPage() {
258261 club_id : myClubId ,
259262 quantity : qty ,
260263 location : newAssetLocation . trim ( ) ,
264+ max_rental_days : newAssetMaxRentalDays || undefined ,
261265 } ) ;
262266
263267 setIsAddingAsset ( false ) ;
@@ -307,10 +311,10 @@ export function AdminDashboardPage() {
307311 '물품명' : 'name' ,
308312 '설명' : 'description' ,
309313 '수량' : 'quantity' ,
310- '위치' : 'location' ,
314+ '위치' : 'location' ,
311315 '사용가능수량' : 'available_quantity' ,
312316 '전체수량' : 'total_quantity' ,
313- '등록일' : 'created_at'
317+ '등록일' : 'created_at'
314318 } ;
315319
316320 interface MappedAssetRow {
@@ -328,73 +332,73 @@ export function AdminDashboardPage() {
328332
329333 // 3. 실제 업로드 실행 핸들러 (모달 내 '업로드' 버튼 클릭 시)
330334 const handleExcelUploadSubmit = async ( ) => {
331- if ( ! selectedExcelFile || myClubId === null ) {
332- alert ( '파일을 선택해주세요.' ) ;
333- return ;
334- }
335+ if ( ! selectedExcelFile || myClubId === null ) {
336+ alert ( '파일을 선택해주세요.' ) ;
337+ return ;
338+ }
335339
336- setIsUploading ( true ) ;
340+ setIsUploading ( true ) ;
337341
338- const reader = new FileReader ( ) ;
339- reader . onload = async ( e ) => {
340- try {
341- const data = e . target ?. result ;
342- const workbook = XLSX . read ( data , { type : 'binary' } ) ;
343-
344- // 2. 데이터 읽기 및 헤더 변환 (한글 -> 영어)
345- const firstSheet = workbook . Sheets [ workbook . SheetNames [ 0 ] ] ;
346- const rawData = XLSX . utils . sheet_to_json < Record < string , unknown > > ( firstSheet ) ;
347-
348- const translatedData = rawData . map ( ( row : RawExcelRow ) => {
349- const newRow : MappedAssetRow = { } ;
350- Object . keys ( row ) . forEach ( koKey => {
351- const enKey = HEADER_MAP [ koKey ] ;
352- if ( enKey ) {
353- ( newRow [ enKey ] as unknown ) = row [ koKey ] ;
354- }
342+ const reader = new FileReader ( ) ;
343+ reader . onload = async ( e ) => {
344+ try {
345+ const data = e . target ?. result ;
346+ const workbook = XLSX . read ( data , { type : 'binary' } ) ;
347+
348+ // 2. 데이터 읽기 및 헤더 변환 (한글 -> 영어)
349+ const firstSheet = workbook . Sheets [ workbook . SheetNames [ 0 ] ] ;
350+ const rawData = XLSX . utils . sheet_to_json < Record < string , unknown > > ( firstSheet ) ;
351+
352+ const translatedData = rawData . map ( ( row : RawExcelRow ) => {
353+ const newRow : MappedAssetRow = { } ;
354+ Object . keys ( row ) . forEach ( koKey => {
355+ const enKey = HEADER_MAP [ koKey ] ;
356+ if ( enKey ) {
357+ ( newRow [ enKey ] as unknown ) = row [ koKey ] ;
358+ }
359+ } ) ;
360+ return newRow ;
355361 } ) ;
356- return newRow ;
357- } ) ;
358362
359- // 3. 수정된 데이터로 새로운 엑셀 파일(워크북) 생성
360- const newWorksheet = XLSX . utils . json_to_sheet ( translatedData ) ;
361- const newWorkbook = XLSX . utils . book_new ( ) ;
362- XLSX . utils . book_append_sheet ( newWorkbook , newWorksheet , 'Sheet1' ) ;
363+ // 3. 수정된 데이터로 새로운 엑셀 파일(워크북) 생성
364+ const newWorksheet = XLSX . utils . json_to_sheet ( translatedData ) ;
365+ const newWorkbook = XLSX . utils . book_new ( ) ;
366+ XLSX . utils . book_append_sheet ( newWorkbook , newWorksheet , 'Sheet1' ) ;
363367
364- // 4. 워크북을 바이너리(ArrayBuffer)로 변환
365- const excelBuffer = XLSX . write ( newWorkbook , { bookType : 'xlsx' , type : 'array' } ) ;
366- const finalFileBlob = new Blob ( [ excelBuffer ] , { type : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' } ) ;
368+ // 4. 워크북을 바이너리(ArrayBuffer)로 변환
369+ const excelBuffer = XLSX . write ( newWorkbook , { bookType : 'xlsx' , type : 'array' } ) ;
370+ const finalFileBlob = new Blob ( [ excelBuffer ] , { type : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' } ) ;
367371
368- // 5. FormData에 담아서 전송
369- const formData = new FormData ( ) ;
370- // 백엔드에서 받는 필드명('file')에 맞춰 Blob을 파일 객체처럼 추가
371- formData . append ( 'file' , finalFileBlob , 'processed_assets.xlsx' ) ;
372+ // 5. FormData에 담아서 전송
373+ const formData = new FormData ( ) ;
374+ // 백엔드에서 받는 필드명('file')에 맞춰 Blob을 파일 객체처럼 추가
375+ formData . append ( 'file' , finalFileBlob , 'processed_assets.xlsx' ) ;
372376
373- const result = await uploadExcelAssets ( formData ) ;
377+ const result = await uploadExcelAssets ( formData ) ;
374378
375- if ( result . success ) {
376- alert ( `${ result . data ?. imported || 0 } 개의 물품이 성공적으로 업로드되었습니다.` ) ;
377- setShowExcelModal ( false ) ;
378- fetchAssets ( myClubId ) ;
379- } else {
380- alert ( result . error || '업로드 실패' ) ;
379+ if ( result . success ) {
380+ alert ( `${ result . data ?. imported || 0 } 개의 물품이 성공적으로 업로드되었습니다.` ) ;
381+ setShowExcelModal ( false ) ;
382+ fetchAssets ( myClubId ) ;
383+ } else {
384+ alert ( result . error || '업로드 실패' ) ;
385+ }
386+ } catch ( error ) {
387+ console . error ( error ) ;
388+ alert ( '파일 처리 중 오류가 발생했습니다.' ) ;
389+ } finally {
390+ setIsUploading ( false ) ;
381391 }
382- } catch ( error ) {
383- console . error ( error ) ;
384- alert ( '파일 처리 중 오류가 발생했습니다.' ) ;
385- } finally {
386- setIsUploading ( false ) ;
387- }
388- } ;
392+ } ;
389393
390- reader . readAsBinaryString ( selectedExcelFile ) ;
391- } ;
394+ reader . readAsBinaryString ( selectedExcelFile ) ;
395+ } ;
392396
393397 const handleExportAssets = ( ) => {
394- if ( assets . length === 0 ) {
395- alert ( '내보낼 데이터가 없습니다.' ) ;
396- return ;
397- }
398+ if ( assets . length === 0 ) {
399+ alert ( '내보낼 데이터가 없습니다.' ) ;
400+ return ;
401+ }
398402
399403 // 1. 데이터 가공: 사용자가 보기 좋은 한글 헤더로 매핑
400404 // Asset 타입의 필드들을 엑셀 열에 맞게 조정합니다.
@@ -436,6 +440,7 @@ export function AdminDashboardPage() {
436440 description : asset . description ,
437441 quantity : asset . total_quantity ,
438442 location : asset . location ,
443+ max_rental_days : asset . max_rental_days || null ,
439444 } ) ;
440445
441446 // 통계 불러오기
@@ -484,6 +489,7 @@ export function AdminDashboardPage() {
484489 description : editingAsset . description . trim ( ) ,
485490 quantity : editingAsset . quantity ,
486491 location : editingAsset . location . trim ( ) ,
492+ max_rental_days : editingAsset . max_rental_days || undefined ,
487493 } ) ;
488494
489495 setIsUpdatingAsset ( false ) ;
@@ -762,6 +768,21 @@ export function AdminDashboardPage() {
762768 placeholder = "예: 동아리방 선반"
763769 />
764770 </ div >
771+ < div className = "form-group" >
772+ < label htmlFor = "asset-max-rental-days" > 최대 대여 일수</ label >
773+ < input
774+ id = "asset-max-rental-days"
775+ type = "text"
776+ inputMode = "numeric"
777+ pattern = "[0-9]*"
778+ value = { newAssetMaxRentalDays ?? '' }
779+ onChange = { ( e ) => {
780+ const val = e . target . value . replace ( / [ ^ 0 - 9 ] / g, '' ) ;
781+ setNewAssetMaxRentalDays ( val === '' ? null : parseInt ( val ) ) ;
782+ } }
783+ placeholder = "미설정 시 제한 없음"
784+ />
785+ </ div >
765786 { addAssetError && < p className = "error-message" > { addAssetError } </ p > }
766787 < div className = "form-actions" >
767788 < button
@@ -841,15 +862,15 @@ export function AdminDashboardPage() {
841862 />
842863 </ div >
843864 < div style = { { marginBottom : '20px' , textAlign : 'right' } } >
844- < button
845- type = "button"
846- className = "member-approve-btn"
847- onClick = { handleExportAssets }
848- style = { { fontSize : '0.8rem' , padding : '6px 12px' , background : '#f3f4f6' , color : '#374151' , border : '1px solid #d1d5db' } }
849- >
850- 📤 현재 자산 목록 내보내기 (.xlsx)
851- </ button >
852- </ div >
865+ < button
866+ type = "button"
867+ className = "member-approve-btn"
868+ onClick = { handleExportAssets }
869+ style = { { fontSize : '0.8rem' , padding : '6px 12px' , background : '#f3f4f6' , color : '#374151' , border : '1px solid #d1d5db' } }
870+ >
871+ 📤 현재 자산 목록 내보내기 (.xlsx)
872+ </ button >
873+ </ div >
853874
854875 { selectedExcelFile && (
855876 < div style = { { marginBottom : '15px' , fontSize : '14px' , color : '#555' } } >
@@ -976,6 +997,7 @@ export function AdminDashboardPage() {
976997 description : asset . description ,
977998 quantity : asset . total_quantity ,
978999 location : asset . location ,
1000+ max_rental_days : asset . max_rental_days || null ,
9791001 } ) ;
9801002 setShowEditModal ( true ) ;
9811003 } }
@@ -1045,6 +1067,21 @@ export function AdminDashboardPage() {
10451067 placeholder = "예: 동아리방 선반"
10461068 />
10471069 </ div >
1070+ < div className = "form-group" >
1071+ < label htmlFor = "edit-max-rental-days" > 최대 대여 일수</ label >
1072+ < input
1073+ id = "edit-max-rental-days"
1074+ type = "text"
1075+ inputMode = "numeric"
1076+ pattern = "[0-9]*"
1077+ value = { editingAsset . max_rental_days ?? '' }
1078+ onChange = { ( e ) => {
1079+ const val = e . target . value . replace ( / [ ^ 0 - 9 ] / g, '' ) ;
1080+ setEditingAsset ( { ...editingAsset , max_rental_days : val === '' ? null : parseInt ( val ) } ) ;
1081+ } }
1082+ placeholder = "미설정 시 제한 없음"
1083+ />
1084+ </ div >
10481085
10491086 { /* 사진 관리 섹션 */ }
10501087 < div className = "picture-section" >
0 commit comments