Skip to content

Commit 5f0fb4c

Browse files
authored
Merge pull request #171 from CSE-Shaco/develop
fix(core-attendance): Axios 인증/경로 버그 수정 및 출석 화면 안정화
2 parents 031aa37 + f464069 commit 5f0fb4c

File tree

1 file changed

+130
-119
lines changed

1 file changed

+130
-119
lines changed

src/app/core-attendance/page.jsx

Lines changed: 130 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,29 @@ import {Button, Card, CardBody, Checkbox, Divider, Input, Select, SelectItem} fr
88
const API = axios.create({
99
baseURL: (process.env.NEXT_PUBLIC_BASE_API_URL?.replace(/\/$/, '') || 'http://localhost:8080') + '/core-attendance/meetings',
1010
timeout: 15000,
11-
withCredentials: true,
11+
withCredentials: true, // refresh_token 쿠키 전송
1212
});
1313

14-
// 액세스 토큰 부착 & 401→refresh 재시도(간단 버전)
15-
const getAccessToken = () => localStorage.getItem('access_token');
14+
// 액세스 토큰 부착 & 401 → refresh 재시도 (간단 버전)
15+
const getAccessToken = () => (typeof window !== 'undefined' ? localStorage.getItem('access_token') : null);
16+
1617
API.interceptors.request.use((config) => {
1718
const t = getAccessToken();
18-
if (t) {
19+
// 'undefined' / 'null' 문자열 방어
20+
if (t && t !== 'undefined' && t !== 'null') {
1921
config.headers = config.headers || {};
2022
config.headers.Authorization = `Bearer ${t}`;
2123
}
2224
return config;
2325
});
26+
2427
API.interceptors.response.use((r) => r, async (err) => {
2528
const original = err.config || {};
2629
if (err.response?.status === 401 && !original.__retry) {
2730
original.__retry = true;
2831
const base = process.env.NEXT_PUBLIC_BASE_API_URL?.replace(/\/$/, '') || 'http://localhost:8080';
2932
try {
33+
// refresh 호출 (쿠키 필요)
3034
const res = await axios.post(`${base}/auth/refresh`, null, {withCredentials: true});
3135
const newAccess = res.data?.data?.accessToken;
3236
if (newAccess) {
@@ -43,21 +47,21 @@ API.interceptors.response.use((r) => r, async (err) => {
4347
});
4448

4549
const api = {
46-
// Dates
47-
getDates: async () => (await API.get(`/`)).data.data, // { dates: [...] }
48-
addDate: async (date) => (await API.post(`/`, {date})).data.data,
49-
deleteDate: async (date) => (await API.delete(`/${date}`)).data.data,
50+
getDates: async () => (await API.get(``)).data.data, // { dates: [...] }
51+
addDate: async (date) => (await API.post(``, {date})).data.data,
52+
deleteDate: async (date) => (await API.delete(`${date}`)).data.data,
5053

5154
// Members (전체 팀 포함)
52-
getMembers: async (date) => (await API.get(`/${date}/members`)).data.data, // [{ userId,name,team,present,... }]
55+
getMembers: async (date) => (await API.get(`${date}/members`)).data.data, // [{ userId,name,team,present,... }]
5356

5457
// Batch save
55-
saveAttendance: async (date, userIds, present) => (await API.put(`/${date}/attendance`, {
56-
userIds, present
58+
saveAttendance: async (date, userIds, present) => (await API.put(`${date}/attendance`, {
59+
userIds,
60+
present
5761
})).data.data,
5862

5963
// Summary(옵션)
60-
summary: async (date) => (await API.get(`/${date}/summary`)).data.data,
64+
summary: async (date) => (await API.get(`${date}/summary`)).data.data,
6165
};
6266

6367
/** ===== 유틸 ===== */
@@ -234,122 +238,129 @@ export default function AttendancePage() {
234238
};
235239

236240
return (<div className="flex flex-col max-w-[1100px] mx-auto min-h-[100svh] py-16 px-6">
237-
<h1 className="font-bold mb-6 text-4xl tablet:text-3xl mobile:text-2xl">출석 관리</h1>
241+
<h1 className="font-bold mb-6 text-4xl tablet:text-3xl mobile:text-2xl">출석 관리</h1>
242+
243+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
244+
{/* 날짜 */}
245+
<Card>
246+
<CardBody className="gap-3">
247+
<div className="flex items-center justify-between">
248+
<b>날짜</b>
249+
<Button size="sm" color="primary" onPress={addToday}>
250+
오늘 추가
251+
</Button>
252+
</div>
238253

239-
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
240-
{/* 날짜 */}
241-
<Card>
242-
<CardBody className="gap-3">
243-
<div className="flex items-center justify-between">
244-
<b>날짜</b>
245-
<Button size="sm" color="primary" onPress={addToday}>
246-
오늘 추가
247-
</Button>
248-
</div>
254+
{/* date picker는 유지(빠른 변경용) */}
255+
<Input type="date" value={date} onChange={(e) => setDate(e.target.value)}/>
256+
<Divider/>
257+
<div className="max-h-[180px] overflow-auto space-y-2">
258+
{dates.map((d) => (<div key={d} className="flex items-center justify-between">
259+
<Button size="sm" variant="light" onPress={() => setDate(d)}>
260+
{d === date ? <b>{d}</b> : d}
261+
</Button>
262+
<Button size="sm" color="danger" variant="flat" onPress={() => removeDate(d)}>
263+
삭제
264+
</Button>
265+
</div>))}
266+
{dates.length === 0 && (<div className="text-sm text-foreground-500">등록된 날짜가 없습니다.</div>)}
267+
</div>
268+
</CardBody>
269+
</Card>
270+
271+
{/* 필터 & 저장 */}
272+
<Card>
273+
<CardBody className="gap-3">
274+
<div className="flex items-center justify-between">
275+
<b>필터 / 저장</b>
276+
<Button
277+
size="sm"
278+
color="primary"
279+
variant="flat"
280+
onPress={saveSnapshot}
281+
isDisabled={!dirty || !members.length}
282+
>
283+
저장{dirty ? ' *' : ''}
284+
</Button>
285+
</div>
249286

250-
{/* date picker는 유지(빠른 변경용) */}
251-
<Input type="date" value={date} onChange={(e) => setDate(e.target.value)}/>
252-
<Divider/>
253-
<div className="max-h-[180px] overflow-auto space-y-2">
254-
{dates.map((d) => (<div key={d} className="flex items-center justify-between">
255-
<Button size="sm" variant="light" onPress={() => setDate(d)}>
256-
{d === date ? <b>{d}</b> : d}
287+
<Select
288+
label="팀(클라이언트 필터)"
289+
selectedKeys={teamFilter ? new Set([teamFilter]) : new Set()}
290+
onSelectionChange={(keys) => {
291+
const first = String(Array.from(keys || [])[0] ?? '');
292+
setTeamFilter(first || '');
293+
}}
294+
variant="bordered"
295+
>
296+
{teamOptions.map((t) => (<SelectItem key={t} value={t}>
297+
{t}
298+
</SelectItem>))}
299+
</Select>
300+
301+
<Input placeholder="이름 검색" value={filter} onValueChange={setFilter} size="sm"/>
302+
303+
<div className="flex gap-2">
304+
<Button size="sm" onPress={() => checkAll(true)} color="success" variant="flat">
305+
(필터된) 전체 체크
257306
</Button>
258-
<Button size="sm" color="danger" variant="flat" onPress={() => removeDate(d)}>
259-
삭제
307+
<Button size="sm" onPress={() => checkAll(false)} color="warning" variant="flat">
308+
(필터된) 전체 해제
260309
</Button>
261-
</div>))}
262-
{dates.length === 0 && (<div className="text-sm text-foreground-500">등록된 날짜가 없습니다.</div>)}
263-
</div>
264-
</CardBody>
265-
</Card>
266-
267-
{/* 필터 & 저장 */}
268-
<Card>
310+
</div>
311+
</CardBody>
312+
</Card>
313+
314+
{/* 요약 */}
315+
<Card>
316+
<CardBody className="gap-3">
317+
<b>요약</b>
318+
{summary ? (<div className="text-sm">
319+
<div className="mb-2">전체 {summary.present} / {summary.total}</div>
320+
<Divider/>
321+
<div className="mt-2 space-y-1">
322+
{summary.perTeam.map((ts) => (
323+
<div key={ts.teamId} className="flex items-center justify-between">
324+
<span>{ts.teamName}</span>
325+
<span>{ts.present} / {ts.total}</span>
326+
</div>))}
327+
</div>
328+
</div>) : (<div className="text-foreground-500 text-sm">로딩...</div>)}
329+
</CardBody>
330+
</Card>
331+
</div>
332+
333+
{/* 팀원 목록 */}
334+
<Card className="mt-6">
269335
<CardBody className="gap-3">
270336
<div className="flex items-center justify-between">
271-
<b>필터 / 저장</b>
272-
<Button size="sm" color="primary" variant="flat" onPress={saveSnapshot}
273-
isDisabled={!dirty || !members.length}>
274-
저장{dirty ? ' *' : ''}
275-
</Button>
337+
<div>
338+
<b>팀원</b>
339+
<div className="text-xs text-foreground-500">
340+
{date} · {teamFilter || '전체 팀'}
341+
</div>
342+
</div>
276343
</div>
277344

278-
<Select
279-
label="팀(클라이언트 필터)"
280-
selectedKeys={teamFilter ? new Set([teamFilter]) : new Set()}
281-
onSelectionChange={(keys) => {
282-
const first = String(Array.from(keys || [])[0] ?? '');
283-
setTeamFilter(first || '');
284-
}}
285-
variant="bordered"
286-
>
287-
{teamOptions.map((t) => (<SelectItem key={t} value={t}>
288-
{t}
289-
</SelectItem>))}
290-
</Select>
291-
292-
<Input placeholder="이름 검색" value={filter} onValueChange={setFilter} size="sm"/>
293-
294-
<div className="flex gap-2">
295-
<Button size="sm" onPress={() => checkAll(true)} color="success" variant="flat">
296-
(필터된) 전체 체크
297-
</Button>
298-
<Button size="sm" onPress={() => checkAll(false)} color="warning" variant="flat">
299-
(필터된) 전체 해제
300-
</Button>
301-
</div>
302-
</CardBody>
303-
</Card>
345+
<Divider/>
304346

305-
{/* 요약 */}
306-
<Card>
307-
<CardBody className="gap-3">
308-
<b>요약</b>
309-
{summary ? (<div className="text-sm">
310-
<div className="mb-2">전체 {summary.present} / {summary.total}</div>
311-
<Divider/>
312-
<div className="mt-2 space-y-1">
313-
{summary.perTeam.map((ts) => (
314-
<div key={ts.teamId} className="flex items-center justify-between">
315-
<span>{ts.teamName}</span>
316-
<span>{ts.present} / {ts.total}</span>
317-
</div>))}
318-
</div>
319-
</div>) : (<div className="text-foreground-500 text-sm">로딩...</div>)}
347+
<div className="max-h-[460px] overflow-auto">
348+
{filteredMembers.map((m) => {
349+
const id = String(m.userId);
350+
const checked = presentSet.has(id);
351+
return (<div key={id} className="flex items-center justify-between py-2">
352+
<div className="flex items-center gap-3">
353+
<Checkbox isSelected={checked} onValueChange={() => toggleMember(m)}>
354+
{m.name}{' '}
355+
<span className="text-xs text-foreground-500 ml-2">({m.team})</span>
356+
</Checkbox>
357+
</div>
358+
</div>);
359+
})}
360+
{filteredMembers.length === 0 && (
361+
<div className="text-sm text-foreground-500 py-3">표시할 팀원이 없습니다.</div>)}
362+
</div>
320363
</CardBody>
321364
</Card>
322-
</div>
323-
324-
{/* 팀원 목록 */}
325-
<Card className="mt-6">
326-
<CardBody className="gap-3">
327-
<div className="flex items-center justify-between">
328-
<div>
329-
<b>팀원</b>
330-
<div className="text-xs text-foreground-500">{date} · {teamFilter || '전체 팀'}</div>
331-
</div>
332-
</div>
333-
334-
<Divider/>
335-
336-
<div className="max-h-[460px] overflow-auto">
337-
{filteredMembers.map((m) => {
338-
const id = String(m.userId);
339-
const checked = presentSet.has(id);
340-
return (<div key={id} className="flex items-center justify-between py-2">
341-
<div className="flex items-center gap-3">
342-
<Checkbox isSelected={checked} onValueChange={() => toggleMember(m)}>
343-
{m.name} <span
344-
className="text-xs text-foreground-500 ml-2">({m.team})</span>
345-
</Checkbox>
346-
</div>
347-
</div>);
348-
})}
349-
{filteredMembers.length === 0 && (
350-
<div className="text-sm text-foreground-500 py-3">표시할 팀원이 없습니다.</div>)}
351-
</div>
352-
</CardBody>
353-
</Card>
354-
</div>);
365+
</div>);
355366
}

0 commit comments

Comments
 (0)