Skip to content

Commit 9acef74

Browse files
committed
feat(getcloser): Prevent direct page access via URL
1 parent 8c1edd9 commit 9acef74

12 files changed

Lines changed: 1838 additions & 588 deletions

File tree

getcloser/frontend/package-lock.json

Lines changed: 1432 additions & 400 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

getcloser/frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"react-dom": "19.2.1",
2323
"tailwind-merge": "^3.3.1",
2424
"tailwindcss-animate": "^1.0.7",
25-
"zustand": "^5.0.8"
25+
"zustand": "^5.0.9"
2626
},
2727
"devDependencies": {
2828
"@eslint/eslintrc": "^3",

getcloser/frontend/src/app/layout.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import { Geist, Geist_Mono, Dongle } from 'next/font/google';
55
import './globals.css';
66
import { Providers } from './providers';
7-
import { usePathname } from 'next/navigation';
87
import Header from '@/components/Header';
8+
import { useNavigationStore } from '../store/navigationStore'; // Import the navigation store
99

1010
const geistSans = Geist({
1111
variable: '--font-geist-sans',
@@ -34,8 +34,8 @@ export default function RootLayout({
3434
}: Readonly<{
3535
children: React.ReactNode;
3636
}>) {
37-
const pathname = usePathname();
38-
const hideHeader = pathname === '/page1';
37+
const { currentPage } = useNavigationStore(); // Get the current page from the store
38+
const hideHeader = currentPage === 'page1'; // Determine if header should be hidden
3939

4040
return (
4141
<html lang="en" style={{ background: 'linear-gradient(to bottom, hsl(160 40% 10%) 0%, hsl(160 40% 15%) 40%, hsl(160 40% 20%) 100%)' }} className="min-h-screen">
@@ -47,7 +47,7 @@ export default function RootLayout({
4747
<body
4848
className={`${geistSans.variable} ${geistMono.variable} ${dongle.variable} antialiased`}
4949
>
50-
{!hideHeader && <Header />}
50+
{!hideHeader && <Header />} {/* Conditionally render the Header */}
5151
<Providers>{children}</Providers>
5252
</body>
5353
</html>
Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
1-
import { redirect } from 'next/navigation';
1+
'use client';
2+
3+
import Page1 from './pages/Page1';
4+
import Page2 from './pages/Page2';
5+
import Page3 from './pages/Page3';
6+
import Page4 from './pages/Page4';
7+
import { useNavigationStore } from '../store/navigationStore';
28

39
export default function Home() {
4-
redirect('/page1');
5-
}
10+
const { currentPage } = useNavigationStore();
11+
12+
const renderPage = () => {
13+
switch (currentPage) {
14+
case 'page1':
15+
return <Page1 />;
16+
case 'page2':
17+
return <Page2 />;
18+
case 'page3':
19+
return <Page3 />;
20+
case 'page4':
21+
return <Page4 />;
22+
default:
23+
return <Page1 />;
24+
}
25+
};
26+
27+
return (
28+
<main className="flex min-h-screen flex-col">
29+
{renderPage()}
30+
</main>
31+
);
32+
}

getcloser/frontend/src/app/page3/page.tsx

Lines changed: 0 additions & 146 deletions
This file was deleted.

getcloser/frontend/src/app/page1/page.tsx renamed to getcloser/frontend/src/app/pages/Page1.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ import { Button } from '@/components/ui/button';
55
import { Input } from '@/components/ui/input';
66
import { Label } from '@/components/ui/label';
77
import { useFormStore } from '../../store/formStore';
8-
9-
import { useRouter } from 'next/navigation';
8+
import { useNavigationStore } from '../../store/navigationStore';
109

1110
export default function Page1() {
1211
const { email, setEmail, setId, setAccessToken } = useFormStore();
13-
const router = useRouter();
12+
const { setCurrentPage } = useNavigationStore();
1413

1514
const handleSubmit = async (e: React.FormEvent) => {
1615
e.preventDefault();
@@ -56,7 +55,7 @@ export default function Page1() {
5655
setId(userMeResult.sub);
5756
}
5857
alert('정보가 제출되었습니다!');
59-
router.push('/page2');
58+
setCurrentPage('page2');
6059
} catch (error) {
6160
console.error('Error submitting form:', error);
6261
alert('정보 제출에 실패했습니다.');

getcloser/frontend/src/app/page2/page.tsx renamed to getcloser/frontend/src/app/pages/Page2.tsx

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@
33
import Avatar from 'boring-avatars';
44
import Cookies from 'js-cookie';
55
import React, { useState, useEffect, useRef, useCallback } from 'react';
6-
import Link from 'next/link';
7-
import { useRouter } from 'next/navigation';
86
import { Button } from '@/components/ui/button';
97
import { Input } from '@/components/ui/input';
108
import { Label } from '@/components/ui/label';
119
import Modal from '@/components/Modal';
1210
import { authenticatedFetch } from '../../lib/api';
1311
import { useFormStore } from '../../store/formStore';
12+
import { useNavigationStore } from '../../store/navigationStore';
1413

1514
type View = 'loading' | 'create' | 'waiting';
1615
type TeamMember = {
@@ -24,6 +23,7 @@ const TEAM_SIZE = process.env.NEXT_PUBLIC_TEAM_SIZE
2423
: 5;
2524

2625
const WaitingView = ({ teamMembers, myId, teamId, setView }: { teamMembers: TeamMember[], myId: number, teamId: number, setView: (view: View) => void }) => {
26+
const { setCurrentPage } = useNavigationStore();
2727
const handleLeaveTeam = async () => {
2828
try {
2929
await authenticatedFetch(`/api/v1/teams/${String(teamId)}/cancel`, {
@@ -89,6 +89,7 @@ const CreateTeamView = ({
8989
}) => {
9090
const [showModal, setShowModal] = useState(false);
9191
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
92+
const { setCurrentPage } = useNavigationStore();
9293

9394
useEffect(() => {
9495
const hasSeenModal = Cookies.get('doNotShowModalPage2');
@@ -107,7 +108,7 @@ const CreateTeamView = ({
107108
<div className="container mx-auto p-4">
108109
<Modal
109110
title="미션 소개"
110-
content={('1. 참가자들의 코드를 모으세요!<br /> 코드는 위에 개인별 다른 코드가 있습니다.<br />2. 5명이 함께 문제 풀기에 도전하세요!<br /> (팁! 문제는 팀원들과 관련된 문제가 나옵니다.)<br />3. 성공 시 부스 방문해주세요.<br /> 성공 선물을 드립니다.')}
111+
content={('1. 참가자들의 코드를 모으세요!\n 코드는 위에 개인별 다른 코드가 있습니다.\n2. 5명이 함께 문제 풀기에 도전하세요!\n (팁! 문제는 팀원들과 관련된 문제가 나옵니다.)\n3. 성공 시 부스 방문해주세요.\n 성공 선물을 드립니다.')}
111112
onConfirm={handleConfirm}
112113
onDoNotShowAgain={handleDoNotShowAgain}
113114
isOpen={showModal}
@@ -133,23 +134,17 @@ const CreateTeamView = ({
133134
<Button onClick={handleCreateTeam} className="w-full">문제 풀기</Button>
134135
</div>
135136
<nav className="flex justify-between mt-8">
136-
<Button asChild className="rounded-full" variant={'outline'}>
137-
<Link href="/page1">&lt;</Link>
137+
<Button onClick={() => setCurrentPage('page1')} className="rounded-full" variant={'outline'}>
138+
&lt;
138139
</Button>
139140
</nav>
140141
</div>
141142
);
142143
};
143144

144145
export default function Page2() {
145-
const { id: myId, teamId, setTeamId, setMemberIds } = useFormStore();
146-
const router = useRouter();
147-
148-
useEffect(() => {
149-
if (!myId) {
150-
router.push('/page1');
151-
}
152-
}, [myId, router]);
146+
const { id: myId, teamId, setTeamId, setMemberIds, reset } = useFormStore();
147+
const { setCurrentPage } = useNavigationStore();
153148

154149
const [view, setView] = useState<View>('loading');
155150
const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]);
@@ -189,6 +184,70 @@ export default function Page2() {
189184
setInputs(newInputs);
190185
}, [myId, inputs, fetchUserDisplayName]);
191186

187+
useEffect(() => {
188+
const hydrateTeam = async () => {
189+
if (myId && teamId && view === 'loading') {
190+
console.log('Hydrating team members from persisted teamId...');
191+
try {
192+
const response = await authenticatedFetch('/api/v1/teams/me');
193+
if (!response.ok) {
194+
const errorData = await response.json();
195+
throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.detail || response.statusText}`);
196+
}
197+
const teamData = await response.json();
198+
if (teamData && teamData.members && teamData.members.length > 0) {
199+
const newInputs: InputState[] = Array(TEAM_SIZE).fill(null).map((_, index) => {
200+
if (index === 0) {
201+
// My ID should be the first input
202+
const me = teamData.members.find((member: { id: number; }) => Number(member.id) === Number(myId));
203+
return me ? { id: String(me.id), displayName: me.name } : { id: String(myId), displayName: '' };
204+
} else {
205+
// Other members
206+
const member = teamData.members[index];
207+
return member ? { id: String(member.id), displayName: member.name } : { id: '', displayName: '' };
208+
}
209+
});
210+
211+
const initialTeamMembers: TeamMember[] = teamData.members.map((member: { id: number; name: string; }) => ({
212+
user_id: Number(member.id),
213+
displayName: member.name,
214+
is_ready: false, // will be updated by polling later
215+
}));
216+
217+
setInputs(newInputs);
218+
setTeamMembers(initialTeamMembers);
219+
setView('waiting');
220+
} else {
221+
console.warn('No team members found in /teams/me response, falling back to create view.');
222+
setView('create');
223+
}
224+
} catch (error) {
225+
console.error('Error hydrating team data:', error);
226+
if (teamId && teamId > 0) {
227+
try {
228+
console.log(`Attempting to leave team ${teamId} due to hydration error...`);
229+
await authenticatedFetch(`/api/v1/teams/${teamId}/cancel`, {
230+
method: 'POST',
231+
});
232+
console.log(`Successfully left team ${teamId}.`);
233+
} catch (cancelError) {
234+
console.error(`Failed to leave team ${teamId} after hydration error:`, cancelError);
235+
}
236+
}
237+
reset();
238+
localStorage.removeItem('lastPage');
239+
setCurrentPage('page1');
240+
}
241+
} else if (!myId) {
242+
setCurrentPage('page1'); // Redirect to page1 if myId is missing
243+
} else if (view === 'loading') {
244+
setView('create'); // Fallback if no teamId but not redirected
245+
}
246+
};
247+
248+
hydrateTeam();
249+
}, [myId, teamId, view, setCurrentPage, reset]);
250+
192251
useEffect(() => {
193252
if (myId && inputs[0].id === '') {
194253
fetchUserById(0, String(myId));
@@ -246,9 +305,9 @@ export default function Page2() {
246305
useEffect(() => {
247306
if (view === 'waiting' && teamMembers.length > 0 && teamMembers.every(m => m.is_ready)) {
248307
setMemberIds(teamMembers.map(m => m.user_id));
249-
router.push('/page3');
308+
setCurrentPage('page3');
250309
}
251-
}, [view, teamMembers, router, setMemberIds]);
310+
}, [view, teamMembers, setCurrentPage, setMemberIds]);
252311

253312
const handleInputChange = (index: number, value: string) => {
254313
const newInputs = [...inputs];

0 commit comments

Comments
 (0)