Skip to content

Commit f4f9748

Browse files
committed
feat: 테이블 디자인 변경
1 parent 457890c commit f4f9748

File tree

4 files changed

+467
-292
lines changed

4 files changed

+467
-292
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { VerifyStatus } from "@/types/scores";
2+
3+
const statusStyles = {
4+
PENDING: "bg-yellow-100 text-yellow-800",
5+
APPROVED: "bg-green-100 text-green-800",
6+
REJECTED: "bg-red-100 text-red-800",
7+
};
8+
9+
const statusLabels = {
10+
PENDING: "대기중",
11+
APPROVED: "승인됨",
12+
REJECTED: "거절됨",
13+
};
14+
15+
interface StatusBadgeProps {
16+
status: VerifyStatus;
17+
}
18+
19+
export function StatusBadge({ status }: StatusBadgeProps) {
20+
return (
21+
<span
22+
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${statusStyles[status]}`}
23+
>
24+
{statusLabels[status]}
25+
</span>
26+
);
27+
}

src/components/scores/gpa/GpaScoreTable.tsx

Lines changed: 152 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ import type { GpaScoreWithUser, VerifyStatus } from "@/types/scores";
44
import { ScoreVerifyButton } from "../ScoreVerifyButton";
55
import { format } from "date-fns";
66
import { toast } from "sonner";
7+
import { StatusBadge } from "../StatusBadge";
8+
import {
9+
Table,
10+
TableBody,
11+
TableCell,
12+
TableHead,
13+
TableHeader,
14+
TableRow,
15+
} from "@/components/ui/table";
716

817
interface Props {
918
verifyFilter: VerifyStatus;
@@ -86,150 +95,153 @@ export function GpaScoreTable({ verifyFilter }: Props) {
8695
}
8796
};
8897

89-
if (loading) return <div>로딩중...</div>;
90-
9198
return (
92-
<div className="overflow-x-auto">
93-
<table className="min-w-full divide-y divide-gray-200">
94-
<thead className="bg-gray-50">
95-
<tr>
96-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
97-
ID
98-
</th>
99-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
100-
닉네임
101-
</th>
102-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
103-
GPA
104-
</th>
105-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
106-
기준점수
107-
</th>
108-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
109-
상태
110-
</th>
111-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
112-
제출일
113-
</th>
114-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
115-
거절사유
116-
</th>
117-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
118-
인증파일
119-
</th>
120-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
121-
작업
122-
</th>
123-
</tr>
124-
</thead>
125-
<tbody className="divide-y divide-gray-200 bg-white">
126-
{scores.map((score) => (
127-
<tr key={score.gpaScoreStatusResponse.id}>
128-
<td className="whitespace-nowrap px-6 py-4">
129-
{score.gpaScoreStatusResponse.id}
130-
</td>
131-
<td className="whitespace-nowrap px-6 py-4">
132-
<div className="flex items-center">
133-
<img
134-
src={score.siteUserResponse.profileImageUrl}
135-
alt="프로필"
136-
className="mr-2 h-8 w-8 rounded-full"
137-
/>
138-
{score.siteUserResponse.nickname}
139-
</div>
140-
</td>
141-
<td className="whitespace-nowrap px-6 py-4">
142-
{editingId === score.gpaScoreStatusResponse.id ? (
143-
<div className="flex gap-2">
144-
<input
145-
type="number"
146-
step="0.01"
147-
value={editingGpa}
148-
onChange={(e) =>
149-
setEditingGpa(parseFloat(e.target.value))
150-
}
151-
className="w-20 rounded border px-2 py-1"
152-
/>
99+
<div className="rounded-lg border bg-white shadow">
100+
<div className="overflow-x-auto">
101+
<Table>
102+
<TableHeader>
103+
<TableRow>
104+
<TableHead>ID</TableHead>
105+
<TableHead>닉네임</TableHead>
106+
<TableHead>GPA</TableHead>
107+
<TableHead>기준점수</TableHead>
108+
<TableHead>상태</TableHead>
109+
<TableHead>제출일</TableHead>
110+
<TableHead>거절사유</TableHead>
111+
<TableHead>인증파일</TableHead>
112+
<TableHead>작업</TableHead>
113+
</TableRow>
114+
</TableHeader>
115+
<TableBody>
116+
{loading ? (
117+
<TableRow>
118+
<TableCell colSpan={9} className="text-center">
119+
<div className="flex items-center justify-center">
120+
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-900"></div>
121+
<span className="ml-2">로딩중...</span>
153122
</div>
154-
) : (
155-
score.gpaScoreStatusResponse.gpaResponse.gpa
156-
)}
157-
</td>
158-
<td className="whitespace-nowrap px-6 py-4">
159-
{editingId === score.gpaScoreStatusResponse.id ? (
160-
<div className="flex gap-2">
161-
<input
162-
type="number"
163-
step="0.01"
164-
value={editingGpaCriteria}
165-
onChange={(e) =>
166-
setEditingGpaCriteria(parseFloat(e.target.value))
167-
}
168-
className="w-20 rounded border px-2 py-1"
123+
</TableCell>
124+
</TableRow>
125+
) : scores.length === 0 ? (
126+
<TableRow>
127+
<TableCell colSpan={9} className="text-center text-gray-500">
128+
데이터가 없습니다
129+
</TableCell>
130+
</TableRow>
131+
) : (
132+
scores.map((score) => (
133+
<TableRow
134+
key={score.gpaScoreStatusResponse.id}
135+
className="hover:bg-gray-50"
136+
>
137+
<TableCell>{score.gpaScoreStatusResponse.id}</TableCell>
138+
<TableCell>
139+
<div className="flex items-center">
140+
<img
141+
src={score.siteUserResponse.profileImageUrl}
142+
alt="프로필"
143+
className="mr-2 h-8 w-8 rounded-full"
144+
/>
145+
{score.siteUserResponse.nickname}
146+
</div>
147+
</TableCell>
148+
<TableCell>
149+
{editingId === score.gpaScoreStatusResponse.id ? (
150+
<div className="flex gap-2">
151+
<input
152+
type="number"
153+
step="0.01"
154+
value={editingGpa}
155+
onChange={(e) =>
156+
setEditingGpa(parseFloat(e.target.value))
157+
}
158+
className="w-20 rounded border px-2 py-1"
159+
/>
160+
</div>
161+
) : (
162+
score.gpaScoreStatusResponse.gpaResponse.gpa
163+
)}
164+
</TableCell>
165+
<TableCell>
166+
{editingId === score.gpaScoreStatusResponse.id ? (
167+
<div className="flex gap-2">
168+
<input
169+
type="number"
170+
step="0.01"
171+
value={editingGpaCriteria}
172+
onChange={(e) =>
173+
setEditingGpaCriteria(parseFloat(e.target.value))
174+
}
175+
className="w-20 rounded border px-2 py-1"
176+
/>
177+
<button
178+
onClick={() => handleSave(score)}
179+
className="rounded bg-blue-500 px-2 py-1 text-white hover:bg-blue-600"
180+
>
181+
저장
182+
</button>
183+
<button
184+
onClick={() => setEditingId(null)}
185+
className="rounded bg-gray-500 px-2 py-1 text-white hover:bg-gray-600"
186+
>
187+
취소
188+
</button>
189+
</div>
190+
) : (
191+
<div className="flex gap-2">
192+
{score.gpaScoreStatusResponse.gpaResponse.gpaCriteria}
193+
<button
194+
onClick={() => handleEdit(score)}
195+
className="rounded bg-gray-100 px-2 py-1 text-gray-600 hover:bg-gray-200"
196+
>
197+
수정
198+
</button>
199+
</div>
200+
)}
201+
</TableCell>
202+
<TableCell>
203+
<StatusBadge
204+
status={score.gpaScoreStatusResponse.verifyStatus}
169205
/>
170-
<button
171-
onClick={() => handleSave(score)}
172-
className="rounded bg-blue-500 px-2 py-1 text-white hover:bg-blue-600"
173-
>
174-
저장
175-
</button>
176-
<button
177-
onClick={() => setEditingId(null)}
178-
className="rounded bg-gray-500 px-2 py-1 text-white hover:bg-gray-600"
206+
</TableCell>
207+
<TableCell>
208+
{format(
209+
new Date(score.gpaScoreStatusResponse.createdAt),
210+
"yyyy-MM-dd HH:mm",
211+
)}
212+
</TableCell>
213+
<TableCell>
214+
{score.gpaScoreStatusResponse.rejectedReason || "-"}
215+
</TableCell>
216+
<TableCell>
217+
<a
218+
href={`${S3_BASE_URL}${score.gpaScoreStatusResponse.gpaResponse.gpaReportUrl}`}
219+
target="_blank"
220+
rel="noopener noreferrer"
221+
className="text-blue-600 hover:text-blue-800 hover:underline"
179222
>
180-
취소
181-
</button>
182-
</div>
183-
) : (
184-
<div className="flex gap-2">
185-
{score.gpaScoreStatusResponse.gpaResponse.gpaCriteria}
186-
<button
187-
onClick={() => handleEdit(score)}
188-
className="rounded bg-gray-100 px-2 py-1 text-gray-600 hover:bg-gray-200"
189-
>
190-
수정
191-
</button>
192-
</div>
193-
)}
194-
</td>
195-
<td className="whitespace-nowrap px-6 py-4">
196-
{score.gpaScoreStatusResponse.verifyStatus}
197-
</td>
198-
<td className="whitespace-nowrap px-6 py-4">
199-
{format(
200-
new Date(score.gpaScoreStatusResponse.createdAt),
201-
"yyyy-MM-dd HH:mm",
202-
)}
203-
</td>
204-
<td className="whitespace-nowrap px-6 py-4">
205-
{score.gpaScoreStatusResponse.rejectedReason || "-"}
206-
</td>
207-
<td className="whitespace-nowrap px-6 py-4">
208-
<a
209-
href={`${S3_BASE_URL}${score.gpaScoreStatusResponse.gpaResponse.gpaReportUrl}`}
210-
target="_blank"
211-
rel="noopener noreferrer"
212-
className="text-blue-600 hover:text-blue-800 hover:underline"
213-
>
214-
파일 보기
215-
</a>
216-
</td>
217-
<td className="whitespace-nowrap px-6 py-4">
218-
<ScoreVerifyButton
219-
currentStatus={score.gpaScoreStatusResponse.verifyStatus}
220-
onVerifyChange={(status, reason) =>
221-
handleVerifyStatus(
222-
score.gpaScoreStatusResponse.id,
223-
status,
224-
reason,
225-
)
226-
}
227-
/>
228-
</td>
229-
</tr>
230-
))}
231-
</tbody>
232-
</table>
223+
파일 보기
224+
</a>
225+
</TableCell>
226+
<TableCell>
227+
<ScoreVerifyButton
228+
currentStatus={score.gpaScoreStatusResponse.verifyStatus}
229+
onVerifyChange={(status, reason) =>
230+
handleVerifyStatus(
231+
score.gpaScoreStatusResponse.id,
232+
status,
233+
reason,
234+
)
235+
}
236+
/>
237+
</TableCell>
238+
</TableRow>
239+
))
240+
)}
241+
</TableBody>
242+
</Table>
243+
</div>
244+
{/* 페이지네이션 추가 예정 */}
233245
</div>
234246
);
235247
}

0 commit comments

Comments
 (0)