@@ -6,7 +6,7 @@ import {useAuthenticatedApi} from '@/hooks/useAuthenticatedApi';
66
77/** ===== 유틸 ===== */
88const 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 ) || '' : '' ) ;
1010const 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