Skip to content

Commit a1cb622

Browse files
authored
Merge pull request #68 from wafflestudio/feature/returndate
feat:return date / type correction,...
2 parents aed981c + 92286bb commit a1cb622

File tree

9 files changed

+668
-118
lines changed

9 files changed

+668
-118
lines changed

src/api/client.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,9 @@ interface AdminSignupRequest {
323323
password: string;
324324
club_name: string;
325325
club_description: string;
326+
club_code?: string;
327+
location_lat: number; // degrees * 1,000,000
328+
location_lng: number; // degrees * 1,000,000
326329
}
327330

328331
interface AdminSignupResponse {
@@ -332,6 +335,8 @@ interface AdminSignupResponse {
332335
club_id: number;
333336
club_name: string;
334337
club_code: string;
338+
location_lat?: number;
339+
location_lng?: number;
335340
}
336341

337342
// 관리자 회원가입
@@ -542,6 +547,31 @@ export const updateClubLocation = async (
542547
}
543548
};
544549

550+
// 동아리 삭제 (DELETE /api/clubs/{club_id}) - 관리자 전용
551+
export const deleteClub = async (clubId: number): Promise<{ success: boolean; error?: string }> => {
552+
try {
553+
const response = await authFetch(`/api/clubs/${clubId}`, {
554+
method: 'DELETE',
555+
});
556+
557+
if (response.status === 204) {
558+
showNotification('동아리가 삭제되었습니다.');
559+
return { success: true };
560+
} else if (response.status === 401) {
561+
return { success: false, error: '인증이 만료되었습니다.' };
562+
} else if (response.status === 403) {
563+
return { success: false, error: '권한이 없습니다.' };
564+
} else if (response.status === 404) {
565+
return { success: false, error: '동아리를 찾을 수 없습니다.' };
566+
} else {
567+
return { success: false, error: '동아리 삭제에 실패했습니다.' };
568+
}
569+
} catch (error) {
570+
console.error('Delete club error:', error);
571+
return { success: false, error: 'Network error occurred' };
572+
}
573+
};
574+
545575
// 대여 이력 조회
546576
export const getSchedules = async (clubId: number, params?: { status?: string; page?: number; size?: number }): Promise<{ success: boolean; data?: SchedulesResponse; error?: string }> => {
547577
try {
@@ -839,6 +869,7 @@ interface AddAssetRequest {
839869
category_id?: number;
840870
quantity: number;
841871
location: string;
872+
max_rental_days?: number;
842873
}
843874

844875
// 관리자: 물품 추가
@@ -891,6 +922,7 @@ export interface Asset {
891922
available_quantity: number;
892923
location: string;
893924
created_at: string;
925+
max_rental_days?: number;
894926
}
895927

896928
// 자산 목록 조회 (GET /api/assets/{club_id}) - 인증 불필요
@@ -920,6 +952,7 @@ interface UpdateAssetRequest {
920952
category_id?: number;
921953
quantity?: number;
922954
location?: string;
955+
max_rental_days?: number;
923956
}
924957

925958
// 관리자: 자산 수정 (PATCH /api/admin/assets/{asset_id})
@@ -1010,8 +1043,8 @@ export const uploadExcelAssets = async (
10101043

10111044
// 사용자: 반납 관련 타입
10121045
interface ReturnResponse {
1013-
id: string;
1014-
item_id: string;
1046+
id: number;
1047+
item_id: number;
10151048
user_id: string;
10161049
status: string;
10171050
borrowed_at: string;
@@ -1151,7 +1184,7 @@ export interface AssetStatistics {
11511184
recent_avg_duration: number;
11521185
unique_borrower_count: number;
11531186
last_borrowed_at: string | null;
1154-
last_updated_at: string | null;
1187+
last_updated_at: string;
11551188
}
11561189

11571190
// 자산 통계 조회 (GET /api/statistics/{asset_id})

src/hooks/useForm.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface UseFormReturn<T> {
1414
handleChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
1515
handleSubmit: (onSubmit: () => Promise<void>) => (e: React.FormEvent) => Promise<void>;
1616
resetForm: () => void;
17+
setFieldValue: <K extends keyof T>(field: K, value: T[K]) => void;
1718
}
1819

1920
export function useForm<T extends object>({
@@ -73,6 +74,10 @@ export function useForm<T extends object>({
7374
setLoading(false);
7475
}, [initialValues]);
7576

77+
const setFieldValue = useCallback(<K extends keyof T>(field: K, value: T[K]) => {
78+
setValues(prev => ({ ...prev, [field]: value }));
79+
}, []);
80+
7681
return {
7782
values,
7883
error,
@@ -82,5 +87,6 @@ export function useForm<T extends object>({
8287
handleChange,
8388
handleSubmit,
8489
resetForm,
90+
setFieldValue,
8591
};
8692
}

src/pages/AdminDashboardPage.tsx

Lines changed: 103 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)