diff --git a/getcloser/frontend/package-lock.json b/getcloser/frontend/package-lock.json
index e328b8e..de11454 100644
--- a/getcloser/frontend/package-lock.json
+++ b/getcloser/frontend/package-lock.json
@@ -11,6 +11,7 @@
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@tanstack/react-query": "^5.90.2",
+ "boring-avatars": "^2.0.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"js-cookie": "^3.0.5",
@@ -2296,6 +2297,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/boring-avatars": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/boring-avatars/-/boring-avatars-2.0.4.tgz",
+ "integrity": "sha512-xhZO/w/6aFmRfkaWohcl2NfyIy87gK5SBbys8kctZeTGF1Apjpv/10pfUuv+YEfVPkESU/h2Y6tt/Dwp+bIZPw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=18.0.0",
+ "react-dom": ">=18.0.0"
+ }
+ },
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
diff --git a/getcloser/frontend/package.json b/getcloser/frontend/package.json
index 6eafefe..0ea7119 100644
--- a/getcloser/frontend/package.json
+++ b/getcloser/frontend/package.json
@@ -12,6 +12,7 @@
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@tanstack/react-query": "^5.90.2",
+ "boring-avatars": "^2.0.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"js-cookie": "^3.0.5",
diff --git a/getcloser/frontend/src/app/globals.css b/getcloser/frontend/src/app/globals.css
index 6cd2925..05ebf1e 100644
--- a/getcloser/frontend/src/app/globals.css
+++ b/getcloser/frontend/src/app/globals.css
@@ -51,3 +51,7 @@ html {
--font-sans: var(--font-dongle);
--font-mono: var(--font-geist-mono);
}
+
+.material-symbols-outlined.filled {
+ font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
+}
diff --git a/getcloser/frontend/src/app/layout.tsx b/getcloser/frontend/src/app/layout.tsx
index 24fea63..e402442 100644
--- a/getcloser/frontend/src/app/layout.tsx
+++ b/getcloser/frontend/src/app/layout.tsx
@@ -39,6 +39,9 @@ export default function RootLayout({
return (
+
+
+
diff --git a/getcloser/frontend/src/app/page2/page.tsx b/getcloser/frontend/src/app/page2/page.tsx
index 74697e0..55b24c6 100644
--- a/getcloser/frontend/src/app/page2/page.tsx
+++ b/getcloser/frontend/src/app/page2/page.tsx
@@ -1,5 +1,6 @@
'use client';
+import Avatar from 'boring-avatars';
import Cookies from 'js-cookie';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import Link from 'next/link';
@@ -11,50 +12,84 @@ import Modal from '@/components/Modal';
import { authenticatedFetch } from '../../lib/api';
import { useFormStore } from '../../store/formStore';
-export default function Page2() {
- const { id, setTeamId, setMemberIds } = useFormStore();
- const router = useRouter();
- const [inputs, setInputs] = useState>(() => {
- const initialInputs = Array(5).fill({ id: '', displayName: '' });
- return initialInputs;
- });
- const [showModal, setShowModal] = useState(false);
- const timeoutRef = useRef(null); // Add timeoutRef
-
- const fetchUserById = useCallback(async (index: number, userId: string) => {
- if (!userId) return; // Don't fetch if ID is empty
+type View = 'loading' | 'create' | 'waiting';
+type TeamMember = {
+ user_id: number;
+ is_ready: boolean;
+ displayName: string;
+};
+type InputState = { id: string; displayName: string };
- // Prevent adding self as a team member
- if (index !== 0 && id && Number(userId) === Number(id)) { // Check only for other team members, not self
- alert(`자기 자신(${userId})을 팀원으로 추가할 수 없습니다.`);
- const newInputs = [...inputs];
- newInputs[index] = { id: '', displayName: '' }; // Clear the input field
- setInputs(newInputs);
- return;
- }
+const WaitingView = ({ teamMembers, myId, teamId }: { teamMembers: TeamMember[], myId: number, teamId: number }) => {
+ const router = useRouter();
+ const handleLeaveTeam = async () => {
try {
- const response = await authenticatedFetch(`/api/v1/users/${userId}`);
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- const userData = await response.json();
- // Assuming the API returns an object with a 'data' field which is the display name
- const newInputs = [...inputs];
- newInputs[index] = { id: userId, displayName: userData.data || userId }; // Store both id and display name
- setInputs(newInputs);
- } catch (error: unknown) {
- let errorMessage = '알 수 없는 오류가 발생했습니다.';
- if (error instanceof Error) {
- errorMessage = error.message;
- }
- console.error(`Error fetching user ${userId}:`, errorMessage);
- const newInputs = [...inputs];
- newInputs[index] = { id: userId, displayName: userId }; // Fallback to just ID if fetch fails
- setInputs(newInputs);
- // Optionally, display an error message to the user
+ await authenticatedFetch(`/api/v1/teams/${teamId}/cancel`, {
+ method: 'POST',
+ });
+ } catch (error) {
+ console.error('Error canceling team:', error);
+ alert('팀을 나가는데 실패했습니다.');
}
- }, [id, inputs]);
+ router.back();
+ };
+
+ const me = teamMembers.find(m => m.user_id === myId);
+
+ return (
+
+
+
+
+
+
팀원 기다리는 중...
+
모든 팀원이 준비되면 퀴즈가 시작됩니다.
+
+
+ {teamMembers.map(member => (
+
+
+
+
+ {member.displayName} {member.user_id === myId ? '(나)' : ''}
+
+
+
+ {member.is_ready ? (
+
check_circle
+ ) : (
+
+ )}
+
+
+ ))}
+
+
+ 팀 나가기
+
+
+
+ );
+};
+
+const CreateTeamView = ({
+ inputs,
+ handleInputChange,
+ handleCreateTeam,
+ fetchUserById
+}: {
+ inputs: InputState[],
+ handleInputChange: (index: number, value: string) => void,
+ handleCreateTeam: () => void,
+ fetchUserById: (index: number, userId: string) => void
+}) => {
+ const [showModal, setShowModal] = useState(false);
+ const timeoutRef = useRef(null);
useEffect(() => {
const hasSeenModal = Cookies.get('doNotShowModalPage2');
@@ -63,133 +98,203 @@ export default function Page2() {
}
}, []);
- // Effect to pre-fill the first input if ID is available
- useEffect(() => {
- if (id && inputs[0].id === '') { // Only fetch if ID exists and input is empty
- fetchUserById(0, id);
- }
- }, [id, fetchUserById, inputs]); // Rerun when id changes
-
- const handleConfirm = () => {
- setShowModal(false);
- };
-
+ const handleConfirm = () => setShowModal(false);
const handleDoNotShowAgain = () => {
- Cookies.set('doNotShowModalPage2', 'true', { expires: 365 }); // Cookie expires in 365 days
+ Cookies.set('doNotShowModalPage2', 'true', { expires: 365 });
setShowModal(false);
};
- const handleInputChange = (index: number, value: string) => {
+ return (
+
+
코드는 위에 개인별 다른 코드가 있습니다.
2. 5명이 함께 문제 풀기에 도전하세요!
(팁! 문제는 팀원들과 관련된 문제가 나옵니다.)
3. 성공 시 부스 방문해주세요.
성공 선물을 드립니다.')}
+ onConfirm={handleConfirm}
+ onDoNotShowAgain={handleDoNotShowAgain}
+ isOpen={showModal}
+ />
+
+ {[...Array(5)].map((_, index) => (
+
+ 팀원 {index + 1} {index === 0 && '(나)'}
+ handleInputChange(index, e.target.value)}
+ onBlur={() => {
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
+ fetchUserById(index, inputs[index].id);
+ }}
+ disabled={index === 0}
+ />
+
+ ))}
+
문제 풀기
+
+
+
+ <
+
+
+
+ );
+};
+
+export default function Page2() {
+ const { id: myId, teamId, setTeamId, setMemberIds } = useFormStore();
+ const router = useRouter();
+
+ const [view, setView] = useState('loading');
+ const [teamMembers, setTeamMembers] = useState([]);
+ const [inputs, setInputs] = useState(() => Array(5).fill({ id: '', displayName: '' }));
+
+ const hasCheckedTeamStatus = useRef(false);
+ const timeoutRef = useRef(null);
+
+ const fetchUserDisplayName = useCallback(async (userId: number | string): Promise => {
+ try {
+ const response = await authenticatedFetch(`/api/v1/users/${userId}`);
+ if (!response.ok) return `User ${userId}`;
+ const data = await response.json();
+ return data.data || `User ${userId}`;
+ } catch (error) {
+ console.error(error);
+ return `User ${userId}`;
+ }
+ }, []);
+
+ const fetchUserById = useCallback(async (index: number, userId: string) => {
+ if (!userId) return;
+ if (index !== 0 && myId && Number(userId) === Number(myId)) {
+ alert(`자기 자신(${userId})을 팀원으로 추가할 수 없습니다.`);
+ const newInputs = [...inputs];
+ newInputs[index] = { id: '', displayName: '' };
+ setInputs(newInputs);
+ return;
+ }
+ const displayName = await fetchUserDisplayName(userId);
const newInputs = [...inputs];
- newInputs[index] = { id: value, displayName: value }; // Temporarily set display name to ID
+ newInputs[index] = { id: userId, displayName: displayName };
setInputs(newInputs);
+ }, [myId, inputs, fetchUserDisplayName]);
+
+ useEffect(() => {
+ if (myId && inputs[0].id === '') {
+ fetchUserById(0, String(myId));
+ }
+ }, [myId, fetchUserById, inputs]);
- // Clear any existing timeout
- if (timeoutRef.current) {
- clearTimeout(timeoutRef.current);
+ useEffect(() => {
+ if (!myId || hasCheckedTeamStatus.current) return;
+
+ const checkUserTeam = async () => {
+ hasCheckedTeamStatus.current = true;
+ try {
+ const response = await authenticatedFetch('/api/v1/teams/create');
+ if (response.ok) {
+ const teamData = await response.json();
+ if (teamData && teamData.status === 'PENDING') {
+ setTeamId(teamData.team_id);
+ setView('waiting');
+ return;
+ }
+ }
+ setView('create');
+ } catch (error) {
+ console.error('Error checking user\'s team status:', error);
+ setView('create');
+ }
+ };
+ checkUserTeam();
+ }, [myId, setTeamId]);
+
+ useEffect(() => {
+ if (view !== 'waiting' || !teamId) return;
+
+ const interval = setInterval(async () => {
+ try {
+ const response = await authenticatedFetch(`/api/v1/teams/${teamId}/status`);
+ if (!response.ok) throw new Error('Failed to fetch team status');
+ const memberStatuses: { user_id: number, is_ready: boolean }[] = await response.json();
+ const membersWithNames = await Promise.all(memberStatuses.map(async (member) => {
+ const existingMember = teamMembers.find(m => m.user_id === member.user_id);
+ const displayName = existingMember?.displayName ?? await fetchUserDisplayName(member.user_id);
+ return { ...member, displayName };
+ }));
+ setTeamMembers(membersWithNames);
+ } catch (error) {
+ console.error('Error polling team status:', error);
+ }
+ }, 2000);
+
+ return () => clearInterval(interval);
+ }, [view, teamId, fetchUserDisplayName, teamMembers]);
+
+ useEffect(() => {
+ if (view === 'waiting' && teamMembers.length > 0 && teamMembers.every(m => m.is_ready)) {
+ setMemberIds(teamMembers.map(m => m.user_id));
+ router.push('/page3');
}
+ }, [view, teamMembers, router, setMemberIds]);
- // Set a new timeout to fetch user data after 3 seconds
+ const handleInputChange = (index: number, value: string) => {
+ const newInputs = [...inputs];
+ newInputs[index] = { id: value, displayName: value };
+ setInputs(newInputs);
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
fetchUserById(index, value);
- }, 3000);
+ }, 1500);
};
- const handleSolveProblem = async () => {
- // Step 1: Check if all inputs are filled
- const allInputsFilled = inputs.every(input => input.id !== '');
- if (!allInputsFilled) {
+ const handleCreateTeam = async () => {
+ if (inputs.some(input => input.id === '')) {
alert('모든 팀원 ID를 채워주세요.');
return;
}
+ const memberIds = inputs.slice(1).map(input => Number(input.id));
+ const requestBody = { my_id: myId, member_ids: memberIds };
- // Step 2: Construct the JSON body
- const myId = id; // id from useFormStore()
- const memberIds = inputs.slice(1).map(input => input.id); // Exclude the first input (my_id)
-
- const requestBody = {
- my_id: myId,
- member_ids: memberIds,
- };
-
- console.log('Request Body:', requestBody);
-
- // Step 3: Make the POST request
try {
const response = await authenticatedFetch('/api/v1/teams/create', {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
+ headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorData = await response.json();
- throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.detail || response.statusText}`);
+ throw new Error(errorData.detail || 'Team creation failed');
}
-
const responseData = await response.json();
- console.log('Team creation successful:', responseData);
- setTeamId(responseData.team_id); // Save team_id to store
- setMemberIds(responseData.members_ids); // Save members_ids to store
- router.push('/page3'); // Navigate to page3 on success
- } catch (error: unknown) {
+ setTeamId(responseData.team_id);
+ setView('waiting');
+ } catch (error) {
console.error('Error creating team:', error);
- let errorMessage = '알 수 없는 오류가 발생했습니다.';
- if (error instanceof Error) {
- errorMessage = error.message;
- }
- alert(`팀 생성에 실패했습니다: ${errorMessage}`);
+ alert(`팀 생성에 실패했습니다: ${error}`);
}
};
- return (
-
-
' +
- ' 코드는 위에 개인별 다른 코드가 있습니다.
' +
- '2. 5명이 함께 문제 풀기에 도전하세요!
' +
- ' (팁! 문제는 팀원들과 관련된 문제가 나옵니다.)
' +
- '3. 성공 시 부스 방문해주세요.
' +
- ' 성공 선물을 드립니다.'
- )}
- onConfirm={handleConfirm}
- onDoNotShowAgain={handleDoNotShowAgain}
- isOpen={showModal}
- />
-
-
- {[...Array(5)].map((_, index) => (
-
- 팀원 {index + 1}
- {index === 0 && (나) }
- handleInputChange(index, e.target.value)}
- onBlur={() => {
- if (timeoutRef.current) {
- clearTimeout(timeoutRef.current); // Clear any pending debounce
- }
- fetchUserById(index, inputs[index].id); // Fetch immediately on blur using the stored ID
- }}
- disabled={index === 0}
- />
-
- ))}
-
문제 풀기
+ if (view === 'loading') {
+ return (
+
+ );
+ }
-
-
- <
-
-
-
+ if (view === 'waiting') {
+ return
;
+ }
+
+ return (
+
);
}