Skip to content

Commit 1b197b0

Browse files
Merge pull request #81 from wafflestudio/fix/asset-pic
Fix/asset pic
2 parents 1911090 + 119e750 commit 1b197b0

File tree

4 files changed

+55
-143
lines changed

4 files changed

+55
-143
lines changed

src/api/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,7 @@ export interface Asset {
925925
location: string;
926926
created_at: string;
927927
max_rental_days?: number;
928+
main_picture?: number;
928929
}
929930

930931
// 자산 목록 조회 (GET /api/assets/{club_id}) - 인증 불필요

src/pages/AdminDashboardPage.tsx

Lines changed: 24 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, useRef, useCallback } from 'react';
1+
import { useState, useEffect, useRef } from 'react';
22
import { getClubMembers, deleteClubMember, addAsset, getAssets, updateAsset, deleteAsset, getMyClubs, uploadExcelAssets, getAssetStatistics, getAssetPictures, addAssetPicture, setMainPicture, deleteAssetPicture, getPictureUrl, getSchedules, type ClubMember, type Asset, type AssetStatistics, type AssetPicture, type Schedule } from '@/api/client';
33
import '@/styles/App.css';
44
import '@/styles/AdminDashboard.css';
@@ -11,8 +11,6 @@ type TabType = 'assets' | 'rentals' | 'members';
1111
interface LazyAssetCardProps {
1212
asset: Asset;
1313
isExpanded: boolean;
14-
mainPictureId: number | null | undefined;
15-
onLoadPicture: (assetId: number) => void;
1614
onClick: () => void;
1715
editingAsset: { name: string; description: string; quantity: number; location: string; max_rental_days: number | null } | null;
1816
statsLoading: boolean;
@@ -21,52 +19,17 @@ interface LazyAssetCardProps {
2119
onEditClick: () => void;
2220
}
2321

24-
function LazyAssetCard({ asset, isExpanded, mainPictureId, onLoadPicture, onClick, editingAsset, statsLoading, statsError, assetStats, onEditClick }: LazyAssetCardProps) {
25-
const cardRef = useRef<HTMLDivElement>(null);
26-
const [hasLoaded, setHasLoaded] = useState(false);
27-
28-
useEffect(() => {
29-
const node = cardRef.current;
30-
if (!node) return;
31-
32-
if (!node) return;
33-
const observer = new IntersectionObserver(
34-
(entries) => {
35-
entries.forEach((entry) => {
36-
if (entry.isIntersecting && !hasLoaded) {
37-
setHasLoaded(true);
38-
onLoadPicture(asset.id);
39-
}
40-
});
41-
},
42-
{ rootMargin: '100px' } // 100px 전에 미리 로드
43-
);
44-
45-
if (cardRef.current) {
46-
observer.observe(cardRef.current);
47-
}
48-
49-
return () => {
50-
if (node) {
51-
observer.unobserve(node);
52-
}
53-
observer.disconnect();
54-
};
55-
}, [asset.id, hasLoaded, onLoadPicture]);
56-
22+
function LazyAssetCard({ asset, isExpanded, onClick, editingAsset, statsLoading, statsError, assetStats, onEditClick }: LazyAssetCardProps) {
5723
return (
58-
<div
59-
ref={cardRef}
60-
className={`asset-card ${isExpanded ? 'expanded' : ''}`}
61-
onClick={onClick}
62-
>
24+
<div className={`asset-card ${isExpanded ? 'expanded' : ''}`} onClick={onClick}>
6325
<div className="asset-card-header">
6426
<div className="asset-image">
65-
{mainPictureId ? (
27+
{asset.main_picture ? (
6628
<img
67-
src={getPictureUrl(mainPictureId)}
29+
src={getPictureUrl(asset.main_picture)}
6830
alt={asset.name}
6931
className="asset-main-picture"
32+
loading="lazy"
7033
/>
7134
) : (
7235
<div className="asset-image-placeholder">📦</div>
@@ -202,9 +165,6 @@ export function AdminDashboardPage() {
202165
const [uploadingPicture, setUploadingPicture] = useState(false);
203166
const pictureInputRef = useRef<HTMLInputElement>(null);
204167

205-
// 각 자산의 대표 사진 ID 저장 (assetId -> pictureId)
206-
const [assetMainPictures, setAssetMainPictures] = useState<Record<number, number | null>>({});
207-
208168
// 동아리 멤버 상태
209169
const [clubMembers, setClubMembers] = useState<ClubMember[]>([]);
210170
const [membersLoading, setMembersLoading] = useState(true);
@@ -233,30 +193,6 @@ export function AdminDashboardPage() {
233193
setAssetsLoading(false);
234194
};
235195

236-
// 개별 자산의 대표 사진 로드 (Intersection Observer용)
237-
const loadAssetMainPicture = useCallback(async (assetId: number) => {
238-
let shouldFetch = false;
239-
240-
// 이미 로드했거나 로딩 중이면 스킵, 아니면 로딩 중 표시 (null로 설정)
241-
setAssetMainPictures(prev => {
242-
if (prev[assetId] !== undefined) {
243-
return prev;
244-
}
245-
shouldFetch = true;
246-
return { ...prev, [assetId]: null };
247-
});
248-
249-
if (!shouldFetch) {
250-
return;
251-
}
252-
253-
const picturesResult = await getAssetPictures(assetId);
254-
if (picturesResult.success && picturesResult.data) {
255-
const mainPic = picturesResult.data.find(p => p.is_main);
256-
setAssetMainPictures(prev => ({ ...prev, [assetId]: mainPic ? mainPic.id : null }));
257-
}
258-
}, []);
259-
260196
// 대여 현황 가져오기 함수
261197
const fetchSchedules = async (clubId: number, status?: string) => {
262198
setSchedulesLoading(true);
@@ -705,94 +641,69 @@ export function AdminDashboardPage() {
705641
});
706642
};
707643

644+
const refreshAssetsList = async () => {
645+
if (myClubId) {
646+
const result = await getAssets(myClubId);
647+
if (result.success && result.data) {
648+
setAssets(result.data);
649+
}
650+
}
651+
};
652+
708653
// 사진 업로드 핸들러
709654
const handlePictureUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
710655
const file = e.target.files?.[0];
711656
if (!file || !expandedAssetId) return;
712657

713-
// 이미지 파일 검증
714-
if (!file.type.startsWith('image/')) {
715-
setError('이미지 파일만 업로드 가능합니다.');
716-
return;
717-
}
718-
719658
setUploadingPicture(true);
720-
721659
try {
722-
// 이미지 압축 (500KB 이상인 경우에만)
723660
let uploadFile = file;
724-
if (file.size > 500 * 1024) {
725-
uploadFile = await compressImage(file);
726-
}
661+
if (file.size > 500 * 1024) uploadFile = await compressImage(file);
727662

728-
const isMain = assetPictures.length === 0; // 첫 번째 사진은 자동으로 대표 설정
663+
const isMain = assetPictures.length === 0;
729664
const result = await addAssetPicture(expandedAssetId, uploadFile, isMain);
730665

731666
if (result.success) {
732667
// 사진 목록 새로고침
733668
const picturesResult = await getAssetPictures(expandedAssetId);
734669
if (picturesResult.success && picturesResult.data) {
735670
setAssetPictures(picturesResult.data);
736-
737-
const newMain = picturesResult.data.find(p => p.is_main) || picturesResult.data[0];
738-
setAssetMainPictures(prev => ({
739-
...prev,
740-
[expandedAssetId]: newMain ? newMain.id : null
741-
}));
742671
}
672+
// 4. 수정: assetMainPictures 업데이트 대신 전체 자산 목록 갱신
673+
refreshAssetsList();
743674
} else {
744675
setError(result.error || '사진 업로드에 실패했습니다.');
745676
}
746677
} catch (err) {
747-
console.error('Image compression error:', err);
678+
console.error('Picture upload error:', err);
748679
setError('이미지 처리 중 오류가 발생했습니다.');
749680
}
750-
751681
setUploadingPicture(false);
752-
753-
// input 초기화
754-
if (pictureInputRef.current) {
755-
pictureInputRef.current.value = '';
756-
}
757682
};
758683

759-
// 대표 사진 설정 핸들러
760684
const handleSetMainPicture = async (pictureId: number) => {
761685
if (!expandedAssetId) return;
762-
763686
const result = await setMainPicture(expandedAssetId, pictureId);
764687
if (result.success) {
765-
// 사진 목록 새로고침
766688
const picturesResult = await getAssetPictures(expandedAssetId);
767689
if (picturesResult.success && picturesResult.data) {
768690
setAssetPictures(picturesResult.data);
769-
770-
setAssetMainPictures(prev => ({
771-
...prev,
772-
[expandedAssetId]: pictureId
773-
}));
774691
}
692+
// 5. 수정: 대표 사진 설정 후 목록 갱신
693+
refreshAssetsList();
775694
} else {
776695
setError(result.error || '대표 사진 설정에 실패했습니다.');
777696
}
778697
};
779698

780-
// 사진 삭제 핸들러
781699
const handleDeletePicture = async (pictureId: number) => {
782700
if (!expandedAssetId) return;
783701
if (!confirm('이 사진을 삭제하시겠습니까?')) return;
784-
785702
const result = await deleteAssetPicture(expandedAssetId, pictureId);
786703
if (result.success) {
787704
setAssetPictures(prev => prev.filter(p => p.id !== pictureId));
788-
789-
setAssetMainPictures(prev => {
790-
if (prev[expandedAssetId] === pictureId) {
791-
return { ...prev, [expandedAssetId]: null };
792-
} else {
793-
return prev;
794-
}
795-
});
705+
// 6. 수정: 사진 삭제 후 목록 갱신 (삭제된 사진이 대표였을 수 있으므로)
706+
refreshAssetsList();
796707
} else {
797708
setError(result.error || '사진 삭제에 실패했습니다.');
798709
}
@@ -1086,8 +997,6 @@ export function AdminDashboardPage() {
1086997
key={asset.id}
1087998
asset={asset}
1088999
isExpanded={expandedAssetId === asset.id}
1089-
mainPictureId={assetMainPictures[asset.id]}
1090-
onLoadPicture={loadAssetMainPicture}
10911000
onClick={() => handleAssetClick(asset)}
10921001
editingAsset={editingAsset}
10931002
statsLoading={statsLoading}

src/pages/ItemListPage.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState, useEffect } from 'react';
22
import { useParams, useNavigate } from 'react-router-dom';
3-
import { getAssets, borrowItem, type Asset } from '@/api/client';
3+
import { getAssets, borrowItem, getPictureUrl, type Asset } from '@/api/client';
44
import '@/styles/App.css';
55

66
const ITEMS_PER_PAGE = 10;
@@ -115,8 +115,16 @@ export function ItemListPage() {
115115
onClick={() => setExpandedAssetId(isExpanded ? null : asset.id)}
116116
style={{ cursor: 'pointer' }}
117117
>
118-
<div className="item-image">
119-
<span style={{ fontSize: '2rem' }}>📦</span>
118+
<div className="item-image" style={{ overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
119+
{asset.main_picture ? (
120+
<img
121+
src={getPictureUrl(asset.main_picture)}
122+
alt={asset.name}
123+
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
124+
/>
125+
) : (
126+
<span style={{ fontSize: '2rem' }}>📦</span>
127+
)}
120128
</div>
121129
<div className="item-content">
122130
<div className="item-header">

src/pages/UserDashboardPage.tsx

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState, useEffect } from 'react';
22
import { useNavigate, useLocation } from 'react-router-dom';
3-
import { applyToClub, getClubMembers, getClub, getSchedules, deleteClubMember, getAssets, getAssetPictures, getPictureUrl, type ClubMember, type Schedule, type Asset, type AssetPicture } from '@/api/client';
3+
import { applyToClub, getClubMembers, getClub, getSchedules, deleteClubMember, getAssets, getPictureUrl, type ClubMember, type Schedule, type Asset } from '@/api/client';
44
import '@/styles/App.css';
55
import '@/styles/AdminDashboard.css';
66

@@ -86,43 +86,35 @@ export function UserDashboardPage() {
8686
]);
8787

8888
if (assetResult.success && assetResult.data) {
89-
await Promise.all(assetResult.data.map(async (asset: Asset) => {
90-
newAssetNames[asset.id] = asset.name;
91-
if (newAssetImages[asset.id]) return;
92-
const picsResult = await getAssetPictures(asset.id);
93-
if (picsResult.success && picsResult.data) {
94-
const mainPic = picsResult.data.find((p: AssetPicture) => p.is_main) || picsResult.data[0];
95-
if (mainPic && mainPic.id) {
96-
newAssetImages[asset.id] = getPictureUrl(mainPic.id);
97-
} else {
89+
// 각 자산을 순회하며 이름과 대표 사진 URL을 매핑합니다.
90+
assetResult.data.forEach((asset: Asset) => {
91+
newAssetNames[asset.id] = asset.name;
92+
93+
// API 추가 호출 없이 main_picture ID가 있으면 바로 URL 생성
94+
if (asset.main_picture) {
95+
newAssetImages[asset.id] = getPictureUrl(asset.main_picture);
96+
} else {
9897
newAssetImages[asset.id] = '';
9998
}
100-
}
101-
}));
102-
}
103-
return { scheduleResult, assetResult };
99+
});
100+
}
101+
return { scheduleResult };
104102
})
105103
);
104+
106105
scheduleResults.forEach((settledResult) => {
107106
if (settledResult.status === 'fulfilled') {
108107
const result = settledResult.value;
109108
if (result.scheduleResult.success && result.scheduleResult.data) {
110109
allSchedules.push(...result.scheduleResult.data.schedules);
111110
}
112-
if (result.assetResult.success && result.assetResult.data) {
113-
result.assetResult.data.forEach(asset => {
114-
newAssetNames[asset.id] = asset.name;
115-
});
116-
setAssetNames(newAssetNames);
117-
setAssetImages(newAssetImages);
118-
setSchedules(allSchedules);
119-
setSchedulesLoading(false);
120-
}
121111
}
122112
});
123113

124-
// 시작일 기준 내림차순 정렬 (최신순)
125114
allSchedules.sort((a, b) => new Date(b.start_date).getTime() - new Date(a.start_date).getTime());
115+
116+
setAssetNames(newAssetNames);
117+
setAssetImages(newAssetImages);
126118
setSchedules(allSchedules);
127119
setSchedulesLoading(false);
128120
};
@@ -338,11 +330,13 @@ export function UserDashboardPage() {
338330
return (
339331
<div key={schedule.id} className="asset-card">
340332
<div className="asset-image">
333+
{/* 최적화된 assetImages 맵 사용 (main_picture ID 기반) */}
341334
{assetImages[schedule.asset_id] ? (
342335
<img
343336
src={assetImages[schedule.asset_id]}
344337
alt="물품 사진"
345338
style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: '8px' }}
339+
loading="lazy"
346340
/>
347341
) : (
348342
<div className="asset-image-placeholder">
@@ -352,7 +346,7 @@ export function UserDashboardPage() {
352346
</div>
353347
<div className="asset-info">
354348
<h3 className="asset-name">
355-
{assetNames[schedule.asset_id] || `물품 ID: ${schedule.asset_id}`}
349+
{assetNames[schedule.asset_id] || `물품 ID: ${schedule.asset_id}`}
356350
</h3>
357351
<p className="asset-detail">
358352
동아리: {clubNames[schedule.club_id] || '로딩중...'}

0 commit comments

Comments
 (0)