Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4fe2587
feat: 출석 조회 api 연결
nabbang6 Apr 6, 2026
8c7527e
feat: 출석 기록 조회 api 연결
nabbang6 Apr 7, 2026
cbb2014
feat: qr 코드 생성 api 연결
nabbang6 Apr 7, 2026
15d7cbe
refactor: 출석 일정이 없는 경우 출석 버튼을 disabled 처리
nabbang6 Apr 8, 2026
598843a
refactor: isAdmin을 useUserRole으로 판단하게 변경
nabbang6 Apr 8, 2026
1ab5311
style: QR 코드 스타일 수정
nabbang6 Apr 8, 2026
85f800b
feat: QR 코드 인식을 통한 출석 체크 기능 구현
nabbang6 Apr 9, 2026
7984bfe
feat: (main) 하위 페이지가 홈을 거치지 않아도 user/club 정보에 접근 가능한 user-hydrator 추가
nabbang6 Apr 9, 2026
28c45c4
refactor: 중복된 user/club 타입 정리 및 통합
nabbang6 Apr 9, 2026
a03bab6
Merge branch 'develop' of https://github.com/Team-Weeth/weeth-client …
nabbang6 Apr 9, 2026
f6e19b6
fix: 출석 기록 조회 시 값이 null일 때 0으로 표시되게 수정
nabbang6 Apr 9, 2026
9181aea
Merge branch 'develop' of https://github.com/Team-Weeth/weeth-client …
nabbang6 Apr 9, 2026
dc1e8a0
Merge branch 'develop' of https://github.com/Team-Weeth/weeth-client …
nabbang6 Apr 12, 2026
485126d
refactor: clubId를 동적으로 가져올 수 있게 수정
nabbang6 Apr 12, 2026
9281b0a
fix: 조회 실패를 에러 토스트로 표시하게 수정
nabbang6 Apr 12, 2026
d6967b1
fix: check-in api 변경사항 반영 (sessionId)
nabbang6 Apr 12, 2026
e3202a5
refactor: QR 코드 생성 로직을 useAttendanceQR 훅으로 분리
nabbang6 Apr 12, 2026
7c261be
refactor: 출석 완료 모달 상태를 AttendanceContent로 통합
nabbang6 Apr 12, 2026
91492f1
refactor: UserHydrator를 동기 hydration 방식으로 변경
nabbang6 Apr 12, 2026
e6c4577
fix: 홈 대시보드 캐시 전략을 ISR로 변경
nabbang6 Apr 12, 2026
8da616a
chore: 불필요한 qr 예시 이미지 삭제
nabbang6 Apr 12, 2026
2f3f518
chore: 불필요한 import 제거
nabbang6 Apr 12, 2026
09c5ce6
fix: useRemainingTime에서 endTime이 없으면 타이머 자체를 시작하지 않도록 수정
nabbang6 Apr 12, 2026
ce774c1
fix: summary가 있을 때만 내용을 렌더링하게 수정
nabbang6 Apr 12, 2026
9735cbc
chore: 사용되지 않는 export 제거
nabbang6 Apr 12, 2026
4bd7b89
fix: endTime가 비어질 때 카운트다운 상태가 초기화되지 않는 문제 수정
nabbang6 Apr 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"lucide-react": "^0.468.0",
"next": "16.1.6",
"posthog-js": "^1.364.4",
"qr-code-styling": "^1.9.2",
"radix-ui": "^1.4.3",
"react": "19.2.3",
"react-dom": "19.2.3",
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 21 additions & 50 deletions src/app/(private)/(main)/attendance/history/page.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,26 @@
import { cookies } from 'next/headers';

import { AttendanceHistoryContent } from '@/components/attendance';
import { CLUB_ID_KEY } from '@/lib/apis/cookies';
import { attendanceServerApi } from '@/lib/apis/attendance.server';
import type { AttendanceSummary } from '@/types/attendance';

// TODO: API 연동 시 실제 데이터로 교체
const mockSummary: AttendanceSummary = {
total: 5,
attendanceCount: 3,
absenceCount: 2,
attendances: [
{
id: 5,
status: 'ABSENT',
title: '5주차 정기모임',
start: '2026-02-23T10:00:00.000Z',
end: '2026-02-23T12:00:00.000Z',
location: '공학관 401호',
},
{
id: 4,
status: 'ATTEND',
title: '4주차 정기모임',
start: '2026-02-16T10:00:00.000Z',
end: '2026-02-16T12:00:00.000Z',
location: '공학관 401호',
},
{
id: 3,
status: 'ABSENT',
title: '3주차 정기모임',
start: '2026-02-09T10:00:00.000Z',
end: '2026-02-09T12:00:00.000Z',
location: '공학관 401호',
},
{
id: 2,
status: 'ATTEND',
title: '2주차 정기모임',
start: '2026-02-02T10:00:00.000Z',
end: '2026-02-02T12:00:00.000Z',
location: '공학관 401호',
},
{
id: 1,
status: 'ATTEND',
title: '1주차 정기모임',
start: '2026-01-26T10:00:00.000Z',
end: '2026-01-26T12:00:00.000Z',
location: '공학관 401호',
},
],
};
export default async function AttendanceHistoryPage() {
const clubId = (await cookies()).get(CLUB_ID_KEY)?.value;

let summary: AttendanceSummary | undefined;
let errorMessage: string | undefined;

if (!clubId) {
errorMessage = '동아리 정보를 찾을 수 없습니다.';
} else {
try {
const response = await attendanceServerApi.getDetail(clubId);
summary = response.data;
} catch {
errorMessage = '출석 기록을 불러오지 못했습니다.';
}
}

export default function AttendanceHistoryPage() {
return <AttendanceHistoryContent summary={mockSummary} />;
return <AttendanceHistoryContent summary={summary} errorMessage={errorMessage} />;
}
52 changes: 33 additions & 19 deletions src/app/(private)/(main)/attendance/page.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
import { cookies } from 'next/headers';

import { AttendanceContent } from '@/components/attendance';
import { CLUB_ID_KEY } from '@/lib/apis/cookies';
import { attendanceServerApi } from '@/lib/apis/attendance.server';
import type { AttendanceData } from '@/types/attendance';

// TODO: API 연동 시 실제 데이터로 교체
function createMockAttendance(): AttendanceData {
const now = new Date();
const start = now;
const end = new Date(now.getTime() + 10 * 60 * 1000); // 10분 후

return {
attendanceRate: 80,
title: '1주차 정기모임',
status: 'ATTEND',
code: 123456,
start: start.toISOString(),
end: end.toISOString(),
location: '공학관 401호',
};
interface AttendancePageProps {
searchParams: Promise<{ sessionId?: string; code?: string }>;
}

export default function AttendancePage() {
// TODO: API 연동 시 실제 사용자 이름으로 교체
const displayName = '사용자';
return <AttendanceContent name={displayName} attendance={createMockAttendance()} />;
export default async function AttendancePage({ searchParams }: AttendancePageProps) {
const { sessionId: qrSessionId, code: qrCode } = await searchParams;

const clubId = (await cookies()).get(CLUB_ID_KEY)?.value;

let attendance: AttendanceData | undefined;
let errorMessage: string | undefined;

if (!clubId) {
errorMessage = '동아리 정보를 찾을 수 없습니다.';
} else {
try {
const response = await attendanceServerApi.getAttendance(clubId);
attendance = response.data;
} catch {
errorMessage = '출석 정보를 불러오지 못했습니다.';
}
}

return (
<AttendanceContent
attendance={attendance}
errorMessage={errorMessage}
qrSessionId={qrSessionId}
qrCode={qrCode}
/>
);
}
39 changes: 26 additions & 13 deletions src/app/(private)/(main)/attendance/qr/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
import { AttendanceQRContent } from '@/components/attendance';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';

// TODO: API 연동 시 실제 데이터로 교체
function createMockQRData() {
const now = new Date();
const end = new Date(now.getTime() + 10 * 60 * 1000); // 10분 후
import { AttendanceQRContent } from '@/components/attendance';
import { CLUB_ID_KEY } from '@/lib/apis/cookies';
import { homeServerApi } from '@/lib/apis/home.server';

return {
title: '1주차 정기모임',
code: '123456',
endTime: end.toISOString(),
};
interface AttendanceQRPageProps {
searchParams: Promise<{ sessionId?: string }>;
}

export default function AttendanceQRPage() {
const { title, code, endTime } = createMockQRData();
return <AttendanceQRContent title={title} code={code} endTime={endTime} />;
export default async function AttendanceQRPage({ searchParams }: AttendanceQRPageProps) {
const { sessionId: rawSessionId } = await searchParams;
const sessionId = Number(rawSessionId);

if (!Number.isInteger(sessionId) || sessionId <= 0) {
redirect('/attendance');
}

const clubId = (await cookies()).get(CLUB_ID_KEY)?.value;
if (!clubId) redirect('/attendance');

const { data } = await homeServerApi.getDashboard(clubId);
const role = data.myInfo.userInfo.role;

if (role !== 'LEAD' && role !== 'ADMIN') {
redirect('/attendance');
}

return <AttendanceQRContent sessionId={sessionId} />;
}
23 changes: 18 additions & 5 deletions src/app/(private)/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
import type { ReactNode } from 'react';
import { cookies } from 'next/headers';

import { homeServerApi } from '@/lib/apis/home.server';
import { CLUB_ID_KEY } from '@/lib/apis/cookies';
import { UserHydrator } from '@/providers/user-hydrator';
import { ClubGuard, Header } from '@/components/layout';

export default function MainLayout({
export default async function MainLayout({
children,
}: Readonly<{
children: ReactNode;
}>) {
const clubId = (await cookies()).get(CLUB_ID_KEY)?.value;
if (!clubId) return null;

const { data } = await homeServerApi.getDashboard(clubId);
const { userInfo } = data.myInfo;
const { id: resolvedClubId, name: clubName } = data.club;

return (
<ClubGuard>
<div className="mx-auto flex h-screen max-w-[1440px] flex-col">
<Header />
{children}
</div>
<UserHydrator userInfo={userInfo} clubInfo={{ clubId: resolvedClubId, clubName }}>
<div className="mx-auto flex h-screen max-w-[1440px] flex-col">
<Header />
{children}
</div>
</UserHydrator>
</ClubGuard>
);
}
Loading
Loading