Skip to content

Commit 67bd7e5

Browse files
authored
Merge pull request #178 from CSE-Shaco/develop
fix(core-attendance): 출석 페이지 스냅샷 저장 방식/필터 개선 및 다크 톤 적용
2 parents 844cd68 + ef9e292 commit 67bd7e5

File tree

1 file changed

+65
-34
lines changed

1 file changed

+65
-34
lines changed

src/app/core-attendance/page.jsx

Lines changed: 65 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {useAuthenticatedApi} from '@/hooks/useAuthenticatedApi';
66

77
/** ===== 유틸 ===== */
88
const ymd = (d = new Date()) => d.toISOString().slice(0, 10);
9-
const getQS = (k) => typeof window !== 'undefined' ? new URL(window.location.href).searchParams.get(k) || '' : '';
9+
const getQS = (k) => (typeof window !== 'undefined' ? new URL(window.location.href).searchParams.get(k) || '' : '');
1010
const setQS = (entries) => {
1111
if (typeof window === 'undefined') return;
1212
const u = new URL(window.location.href);
@@ -28,30 +28,24 @@ export default function AttendancePage() {
2828

2929
// UI
3030
const [filter, setFilter] = useState('');
31-
const [teamFilter, setTeamFilter] = useState(''); // 팀 라벨 기준 필터
31+
const [teamFilter, setTeamFilter] = useState(''); // 팀 라벨 기준 필터 (''=전체)
3232
const [presentSet, setPresentSet] = useState(new Set()); // Set<string(userId)>
3333
const [dirty, setDirty] = useState(false);
3434

3535
/** ===== API 래퍼 ===== */
3636
const api = {
37-
// Dates
3837
getDates: async () => (await apiClient.get('/core-attendance/meetings')).data.data, // { dates: [...] }
3938
addDate: async (d) => (await apiClient.post('/core-attendance/meetings', {date: d})).data.data,
4039
deleteDate: async (d) => (await apiClient.delete(`/core-attendance/meetings/${d}`)).data.data,
4140

42-
// Teams
4341
getTeams: async () => (await apiClient.get('/core-attendance/meetings/teams')).data.data,
44-
45-
// Members (전체 팀 포함)
4642
getMembers: async (d) => (await apiClient.get(`/core-attendance/meetings/${d}/members`)).data.data,
4743

48-
// Batch save
4944
saveAttendance: async (d, userIds, present) => (await apiClient.put(`/core-attendance/meetings/${d}/attendance`, {
5045
userIds,
5146
present
5247
})).data.data,
5348

54-
// Summary
5549
summary: async (d) => (await apiClient.get(`/core-attendance/meetings/${d}/summary`)).data.data,
5650
};
5751

@@ -64,7 +58,7 @@ export default function AttendancePage() {
6458
useEffect(() => {
6559
(async () => {
6660
try {
67-
const dl = await api.getDates(); // { dates: [...] }
61+
const dl = await api.getDates();
6862
setDates(dl.dates);
6963
if (!dl.dates.includes(date) && dl.dates.length > 0) setDate(dl.dates[0]);
7064
} catch {
@@ -80,6 +74,7 @@ export default function AttendancePage() {
8074
try {
8175
const list = await api.getTeams();
8276
setTeams(Array.isArray(list) ? list : []);
77+
// 자동 선택 UX(리드 등 팀 1개만 내려오면 자동 선택)
8378
if (!teamFilter && list?.length === 1) setTeamFilter(list[0].name);
8479
} catch {
8580
setTeams([]);
@@ -105,7 +100,8 @@ export default function AttendancePage() {
105100
setDirty(false);
106101
}
107102
})();
108-
}, [date]); // eslint-disable-line react-hooks/exhaustive-deps
103+
// eslint-disable-next-line react-hooks/exhaustive-deps
104+
}, [date]);
109105

110106
/** 요약 로드 */
111107
useEffect(() => {
@@ -118,7 +114,8 @@ export default function AttendancePage() {
118114
setSummary(null);
119115
}
120116
})();
121-
}, [date]); // eslint-disable-line react-hooks/exhaustive-deps
117+
// eslint-disable-next-line react-hooks/exhaustive-deps
118+
}, [date]);
122119

123120
/** 팀 옵션(라벨) */
124121
const teamOptions = useMemo(() => Array.from(new Set(teams.map((t) => t.name))).filter(Boolean), [teams]);
@@ -179,14 +176,11 @@ export default function AttendancePage() {
179176
setDirty(true);
180177
};
181178

182-
/** 저장(스냅샷) – present=true & present=false 두 번 호출
183-
* ⛳ 서버가 List<Long>을 받으므로 반드시 숫자로 보냄!
184-
*/
179+
/** 저장(스냅샷) – present=true & present=false 두 번 호출 (서버는 List<Long> 기대 → 숫자로 전송) */
185180
const saveSnapshot = async () => {
186181
const allIdsStr = members.map((m) => String(m.userId));
187182
const presentIdsStr = allIdsStr.filter((id) => presentSet.has(id));
188183
const absentIdsStr = allIdsStr.filter((id) => !presentSet.has(id));
189-
// 숫자(Long) 배열로 변환
190184
const presentIds = presentIdsStr.map((s) => Number(s));
191185
const absentIds = absentIdsStr.map((s) => Number(s));
192186

@@ -196,7 +190,7 @@ export default function AttendancePage() {
196190
setDirty(false);
197191
await refreshSummary();
198192
alert('저장되었습니다.');
199-
} catch (e) {
193+
} catch {
200194
alert('저장 중 오류가 발생했습니다.');
201195
}
202196
};
@@ -209,39 +203,48 @@ export default function AttendancePage() {
209203
}
210204
};
211205

212-
return (<div className="flex flex-col max-w-[1100px] mx-auto min-h-[100svh] py-16 px-6">
213-
<h1 className="font-bold mb-6 text-4xl tablet:text-3xl mobile:text-2xl">출석 관리</h1>
206+
return (<div className="dark flex flex-col max-w-[1100px] mx-auto min-h-[100svh] py-16 px-6">
207+
<h1 className="font-bold mb-6 text-4xl tablet:text-3xl mobile:text-2xl text-white">출석 관리</h1>
214208

215209
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
216210
{/* 날짜 */}
217-
<Card>
218-
<CardBody className="gap-3">
211+
<Card className="bg-default-100 dark:bg-default-50">
212+
<CardBody className="gap-3 text-white">
219213
<div className="flex items-center justify-between">
220214
<b>날짜</b>
221215
<Button size="sm" color="primary" onPress={addToday}>
222216
오늘 추가
223217
</Button>
224218
</div>
225219

226-
<Input type="date" value={date} onChange={(e) => setDate(e.target.value)}/>
220+
<Input
221+
type="date"
222+
value={date}
223+
onChange={(e) => setDate(e.target.value)}
224+
classNames={{
225+
input: 'bg-transparent text-black/90 dark:text-white/90',
226+
inputWrapper: 'shadow-xl bg-default-200/50 dark:bg-default/60 hover:bg-default-200/70 dark:hover:bg-default/70',
227+
}}
228+
/>
229+
227230
<Divider/>
228231
<div className="max-h-[180px] overflow-auto space-y-2">
229232
{dates.map((d) => (<div key={d} className="flex items-center justify-between">
230-
<Button size="sm" variant="light" onPress={() => setDate(d)}>
233+
<Button size="sm" variant="light" onPress={() => setDate(d)} className="text-white">
231234
{d === date ? <b>{d}</b> : d}
232235
</Button>
233236
<Button size="sm" color="danger" variant="flat" onPress={() => removeDate(d)}>
234237
삭제
235238
</Button>
236239
</div>))}
237-
{dates.length === 0 && (<div className="text-sm text-foreground-500">등록된 날짜가 없습니다.</div>)}
240+
{dates.length === 0 && <div className="text-sm text-foreground-500">등록된 날짜가 없습니다.</div>}
238241
</div>
239242
</CardBody>
240243
</Card>
241244

242245
{/* 필터 & 저장 */}
243-
<Card>
244-
<CardBody className="gap-3">
246+
<Card className="bg-default-100 dark:bg-default-50">
247+
<CardBody className="gap-3 text-white">
245248
<div className="flex items-center justify-between">
246249
<b>필터 / 저장</b>
247250
<Button
@@ -255,21 +258,45 @@ export default function AttendancePage() {
255258
</Button>
256259
</div>
257260

261+
{/* 팀 선택(“전체” 포함) */}
258262
<Select
259263
label="팀(클라이언트 필터)"
260-
selectedKeys={teamFilter ? new Set([teamFilter]) : new Set()}
264+
selectedKeys={teamFilter ? new Set([teamFilter]) : new Set(['전체'])}
261265
onSelectionChange={(keys) => {
262266
const first = String(Array.from(keys || [])[0] ?? '');
263-
setTeamFilter(first || '');
267+
setTeamFilter(first === '전체' ? '' : first);
264268
}}
265269
variant="bordered"
270+
classNames={{
271+
trigger: 'bg-default-200/50 dark:bg-default/60',
272+
label: 'text-black/50 dark:text-white/90',
273+
value: 'text-black/90 dark:text-white/90',
274+
popoverContent: 'bg-default-100 dark:bg-default-50',
275+
}}
266276
>
267-
{teamOptions.map((name) => (<SelectItem key={name} value={name}>
277+
<SelectItem key="전체" value="전체" className="text-white">
278+
전체
279+
</SelectItem>
280+
{teamOptions.map((name) => (<SelectItem key={name} value={name} className="text-white">
268281
{name}
269282
</SelectItem>))}
270283
</Select>
271284

272-
<Input placeholder="이름 검색" value={filter} onValueChange={setFilter} size="sm"/>
285+
{/* 이름 검색 */}
286+
<Input
287+
placeholder="이름 검색"
288+
value={filter}
289+
onValueChange={setFilter}
290+
size="sm"
291+
isClearable
292+
classNames={{
293+
label: 'text-black/50 dark:text-white/90',
294+
input: ['bg-transparent', 'text-black/90 dark:text-white/90', 'placeholder:text-default-700/50 dark:placeholder:text-white/60',],
295+
innerWrapper: 'bg-transparent',
296+
inputWrapper: ['shadow-xl', 'bg-default-200/50', 'dark:bg-default/60', 'backdrop-blur-xl', 'backdrop-saturate-200', 'hover:bg-default-200/70', 'dark:hover:bg-default/70', 'group-data-[focus=true]:bg-default-200/50', 'dark:group-data-[focus=true]:bg-default/60', '!cursor-text',].join(' '),
297+
}}
298+
onClear={() => setFilter('')}
299+
/>
273300

274301
<div className="flex gap-2">
275302
<Button size="sm" onPress={() => checkAll(true)} color="success" variant="flat">
@@ -283,8 +310,8 @@ export default function AttendancePage() {
283310
</Card>
284311

285312
{/* 요약 */}
286-
<Card>
287-
<CardBody className="gap-3">
313+
<Card className="bg-default-100 dark:bg-default-50">
314+
<CardBody className="gap-3 text-white">
288315
<b>요약</b>
289316
{summary ? (<div className="text-sm">
290317
<div className="mb-2">전체 {summary.present} / {summary.total}</div>
@@ -302,8 +329,8 @@ export default function AttendancePage() {
302329
</div>
303330

304331
{/* 팀원 목록 */}
305-
<Card className="mt-6">
306-
<CardBody className="gap-3">
332+
<Card className="mt-6 bg-default-100 dark:bg-default-50">
333+
<CardBody className="gap-3 text-white">
307334
<div className="flex items-center justify-between">
308335
<div>
309336
<b>팀원</b>
@@ -321,7 +348,11 @@ export default function AttendancePage() {
321348
const checked = presentSet.has(id);
322349
return (<div key={id} className="flex items-center justify-between py-2">
323350
<div className="flex items-center gap-3">
324-
<Checkbox isSelected={checked} onValueChange={() => toggleMember(m)}>
351+
<Checkbox
352+
isSelected={checked}
353+
onValueChange={() => toggleMember(m)}
354+
classNames={{label: 'text-white'}}
355+
>
325356
{m.name}{' '}
326357
<span className="text-xs text-foreground-500 ml-2">({m.team})</span>
327358
</Checkbox>

0 commit comments

Comments
 (0)