diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index f5f4b9a..9462202 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -20,8 +20,6 @@ jobs: node-version: '20.11.1' - name: Build & Export - env: - VITE_API_BASE_URL: ${{ secrets.VITE_API_BASE_URL }} run: | yarn install yarn build diff --git a/.github/workflows/deploy-temp.yml b/.github/workflows/deploy-temp.yml new file mode 100644 index 0000000..1a60ce2 --- /dev/null +++ b/.github/workflows/deploy-temp.yml @@ -0,0 +1,34 @@ +name: deploy-temp + +on: + push: + branches: + - revert-api + workflow_dispatch: + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20.11.1' + + - name: Build & Export + run: | + yarn install + yarn build + + - name: Deploy to S3 and Invalidate Cloudfront + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ap-northeast-2 + run: | + aws s3 sync ./dist s3://waffle-team4-web-temp --delete + aws cloudfront create-invalidation --distribution-id E2HDLBJVBNM8HH --paths "/*" \ No newline at end of file diff --git a/index.html b/index.html index 5cb5702..fb47cdf 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - 모이샤 dev + 모이샤 Alpha
diff --git a/src/api/apiClient.ts b/src/api/apiClient.ts index b55bdbc..bfa312d 100644 --- a/src/api/apiClient.ts +++ b/src/api/apiClient.ts @@ -3,9 +3,8 @@ import useAuthStore from '../hooks/useAuthStore'; // 공통 설정을 가진 axios 인스턴스 생성 const apiClient = axios.create({ - baseURL: `{import.meta.env.VITE_API_BASE_URL}/api/v1`, + baseURL: '/api', // 모든 요청의 기본 경로 timeout: 5000, - headers: { 'Content-Type': 'application/json' }, }); // 요청 인터셉터: 모든 요청 직전에 실행됨 diff --git a/src/api/auth/login.ts b/src/api/auth/login.ts index abd9520..dd86a82 100644 --- a/src/api/auth/login.ts +++ b/src/api/auth/login.ts @@ -1,9 +1,7 @@ -import apiClient from '@/api/apiClient'; -import type { LoginRequest, LoginResponse } from '@/types/auth'; +import type { AuthResponse, LoginRequest } from '../../types/auth'; +import apiClient from '../apiClient'; -export default async function login( - data: LoginRequest -): Promise { - const response = await apiClient.post('/auth/login', data); +export default async function login(data: LoginRequest): Promise { + const response = await apiClient.post('/auth/login', data); return response.data; } diff --git a/src/api/auth/logout.ts b/src/api/auth/logout.ts deleted file mode 100644 index 50ff457..0000000 --- a/src/api/auth/logout.ts +++ /dev/null @@ -1,7 +0,0 @@ -import apiClient from '@/api/apiClient'; -import type { LogoutResponse } from '@/types/auth'; - -export default async function logout(): Promise { - const response = await apiClient.post('/auth/logout'); - return response.data; -} diff --git a/src/api/auth/me.ts b/src/api/auth/me.ts new file mode 100644 index 0000000..62b5902 --- /dev/null +++ b/src/api/auth/me.ts @@ -0,0 +1,7 @@ +import type { User, UserGroup } from '../../types/auth'; +import apiClient from '../apiClient'; + +export default async function getMe(): Promise { + const response = await apiClient.get('/auth/me'); + return response.data; +} diff --git a/src/api/auth/signup.ts b/src/api/auth/signup.ts index 8ed8fab..94b341f 100644 --- a/src/api/auth/signup.ts +++ b/src/api/auth/signup.ts @@ -1,23 +1,19 @@ -import apiClient from '@/api/apiClient'; -import type { SignUpRequest, SignUpResponse } from '@/types/auth'; +import type { AuthResponse, SignUpRequest } from '../../types/auth'; +import apiClient from '../apiClient'; export default async function signup( data: SignUpRequest -): Promise { +): Promise { const formData = new FormData(); - formData.append('email', data.email); - formData.append('name', data.name); + formData.append('username', data.username); formData.append('password', data.password); + formData.append('name', data.name); - if (data.profileImage) { - formData.append('profileImage', data.profileImage); + if (data.photo) { + formData.append('photo', data.photo); // 백엔드 필드명과 'photo' 일치시키기 } - const response = await apiClient.post( - '/auth/signup', - formData - ); - + const response = await apiClient.post('/auth/signup', formData); return response.data; } diff --git a/src/api/auth/social.ts b/src/api/auth/social.ts index e5826e9..d9468e0 100644 --- a/src/api/auth/social.ts +++ b/src/api/auth/social.ts @@ -1,12 +1,9 @@ -import type { SocialLoginRequest, SocialLoginResponse } from '../../types/auth'; +import type { AuthResponse, SocialLoginRequest } from '../../types/auth'; import apiClient from '../apiClient'; export default async function social( data: SocialLoginRequest -): Promise { - const response = await apiClient.post( - '/auth/social', - data - ); +): Promise { + const response = await apiClient.post('/auth/social', data); return response.data; } diff --git a/src/api/event.ts b/src/api/event.ts deleted file mode 100644 index dea5a2f..0000000 --- a/src/api/event.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { - EventDetailResponse, - JoinEventRequest, - JoinEventResponse, - RegistrationListResponse, -} from '@/types/events'; -import apiClient from './apiClient'; - -// 일정 상세 응답 (GET /api/events/:id) -export const getEventDetail = async (id: string) => { - const response = await apiClient.get(`/events/${id}`); - return { - data: response.data, - status: response.status, - }; -}; - -// 참여자 명단 (GET /api/events/:id/registrations) -export const getRegistrations = async (id: string) => { - const response = await apiClient.get( - `/events/${id}/registrations` - ); - return response.data; -}; - -// 참여 신청 (POST /api/events/:id/registrations) -export const registerEvent = async (id: string, data: JoinEventRequest) => { - const response = await apiClient.post( - `/events/${id}/registrations`, - data - ); - return { - data: response.data, - status: response.status, - }; -}; - -// 이벤트 신청 취소(POST /api/v1/events/{eventId}/registrations/{registrationId}/cancel) -// export const cancelRegistration = async ( -// eventId: string, -// registrationId: number, -// token: string -// ) => { -// const response = await apiClient.post( -// `/events/${eventId}/registrations/${registrationId}/cancel`, -// null, // body는 비우고 query parameter로 전달 -// { params: { token } } -// ); -// return response.data; -// }; diff --git a/src/api/users/me.ts b/src/api/users/me.ts deleted file mode 100644 index e957589..0000000 --- a/src/api/users/me.ts +++ /dev/null @@ -1,7 +0,0 @@ -import apiClient from '@/api/apiClient'; -import type { GetMeResponse } from '@/types/users'; - -export default async function getMe(): Promise { - const response = await apiClient.get('/users/me'); - return response.data; -} diff --git a/src/components/EventCardView.tsx b/src/components/EventCardView.tsx index 1a8827e..50745ef 100644 --- a/src/components/EventCardView.tsx +++ b/src/components/EventCardView.tsx @@ -7,29 +7,31 @@ import { ItemMedia, ItemTitle, } from '@/components/ui/item'; -import type { Event } from '@/types/schemas'; +import type { Events } from '@/types/schema'; import { getShortenedDate } from '@/utils/date'; import { ChevronRightIcon, Plus } from 'lucide-react'; import { Link } from 'react-router'; import { Button } from './ui/button'; +type Event = Events & { guests: number; registration_start: string | null }; + function getDateLabel( - startEpoch: number | undefined, - endEpoch: number | undefined + startDateString: string | null, + endDateString: string | null ) { - if (!endEpoch) { + if (!endDateString) { return '상시 모집 중'; } const now = new Date(); - const endDate = new Date(endEpoch); + const endDate = new Date(endDateString!); - if (startEpoch && now < new Date(startEpoch)) { - return `${getShortenedDate(startEpoch)}부터 모집`; + if (startDateString && now < new Date(startDateString)) { + return `${getShortenedDate(startDateString)}부터 모집`; } if (now <= endDate) { - return `${getShortenedDate(endEpoch)}까지 모집`; + return `${getShortenedDate(endDateString)}까지 모집`; } return '모집 마감'; @@ -82,7 +84,10 @@ export default function EventCardView({ events }: { events: Event[] }) { - {getDateLabel(undefined, event.registrationDeadline)} + {getDateLabel( + event.registration_start, + event.registration_deadline + )} diff --git a/src/components/EventDetailContent.tsx b/src/components/EventDetailContent.tsx index 1cb910e..6fba772 100644 --- a/src/components/EventDetailContent.tsx +++ b/src/components/EventDetailContent.tsx @@ -1,11 +1,10 @@ import { ScrollArea } from '@/components/ui/scroll-area'; -import type { Event } from '@/types/schemas'; import { useNavigate } from 'react-router'; +import type { Events } from '../types/schema'; import { formatEventDate } from '../utils/date'; -interface EventDetailContentProps { - schedule: Event; - currentParticipants: number; +interface Props { + schedule: Events; } // 아이콘 @@ -24,10 +23,7 @@ const IconChevronLeft = () => ( ); -export default function EventDetailContent({ - schedule, - currentParticipants, -}: EventDetailContentProps) { +export default function EventDetailContent({ schedule }: Props) { const navigate = useNavigate(); return ( @@ -35,7 +31,7 @@ export default function EventDetailContent({ {/* 1. 일시 및 장소 */}

- 일시 {formatEventDate(schedule.startAt)} + 일시 {formatEventDate(schedule.start_at)}

장소 {schedule.location || '미정'} @@ -44,12 +40,12 @@ export default function EventDetailContent({ {/* 2. 신청 현황 버튼 */}

diff --git a/src/components/ProfileButton.tsx b/src/components/ProfileButton.tsx index de2f3f7..9632e6e 100644 --- a/src/components/ProfileButton.tsx +++ b/src/components/ProfileButton.tsx @@ -8,7 +8,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import type { User } from '@/types/schemas'; +import type { User } from '@/types/auth'; interface ProfileButtonProps { user: User; diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 1a967be..d55a543 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -1,15 +1,14 @@ -import loginApi from '@/api/auth/login'; -import logoutApi from '@/api/auth/logout'; -import signupApi from '@/api/auth/signup'; -import socialApi from '@/api/auth/social'; -import getMeApi from '@/api/users/me'; -import useAuthStore from '@/hooks/useAuthStore'; +import { useNavigate } from 'react-router'; +import loginApi from '../api/auth/login'; +import getMeApi from '../api/auth/me'; +import signupApi from '../api/auth/signup'; +import socialApi from '../api/auth/social'; import type { LoginRequest, SignUpRequest, SocialLoginRequest, -} from '@/types/auth'; -import { useNavigate } from 'react-router'; +} from '../types/auth'; +import useAuthStore from './useAuthStore'; export default function useAuth() { const navigate = useNavigate(); @@ -21,8 +20,7 @@ export default function useAuth() { // 1. 이메일 로그인 로직 const handleLogin = async (data: LoginRequest) => { try { - const { token } = await loginApi(data); - const user = await getMeApi(); + const { user, token } = await loginApi(data); login(user, token); // Zustand 스토어 업데이트 navigate('/'); // 메인 페이지로 이동 } catch (error) { @@ -34,8 +32,9 @@ export default function useAuth() { // 2. 이메일 회원가입 로직 const handleSignUp = async (data: SignUpRequest) => { try { - await signupApi(data); - navigate('/login'); + const { user, token } = await signupApi(data); + login(user, token); + navigate('/'); } catch (error) { console.error('Signup failed:', error); alert('회원가입 중 오류가 발생했습니다.'); @@ -45,7 +44,7 @@ export default function useAuth() { // 3. 소셜 로그인 로직 const handleSocialLogin = async (data: SocialLoginRequest) => { try { - const { token, user } = await socialApi(data); + const { user, token } = await socialApi(data); login(user, token); navigate('/'); } catch (error) { @@ -69,12 +68,16 @@ export default function useAuth() { }; // 5. 로그아웃 로직 - const handleLogout = async () => { - await logoutApi(); + const handleLogout = () => { logout(); // Zustand 상태 초기화 navigate('/login'); }; + // 6. 관리자 확인 + const isAdmin = (ownerId: number | undefined) => { + return isLoggedIn && user?.id === ownerId; + }; + return { user, isLoggedIn, @@ -83,5 +86,6 @@ export default function useAuth() { handleSocialLogin, refreshUser, handleLogout, + isAdmin, }; } diff --git a/src/hooks/useAuthStore.ts b/src/hooks/useAuthStore.ts index 11aebb1..05d00db 100644 --- a/src/hooks/useAuthStore.ts +++ b/src/hooks/useAuthStore.ts @@ -1,6 +1,6 @@ -import type { User } from '@/types/schemas'; import { create } from 'zustand'; import { persist } from 'zustand/middleware'; +import type { User } from '../types/auth'; interface AuthState { user: User | null; diff --git a/src/hooks/useEventDetail.ts b/src/hooks/useEventDetail.ts deleted file mode 100644 index b353cfc..0000000 --- a/src/hooks/useEventDetail.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { JoinEventRequest } from '@/types/events'; -import type { Event, Guest } from '@/types/schemas'; -import { isAxiosError } from 'axios'; -import { useCallback, useState } from 'react'; -import { toast } from 'sonner'; -import { getEventDetail, getRegistrations, registerEvent } from '../api/event'; - -type EventFetchStatus = - | 'GUEST' - | 'ADMIN' - | 'REGISTERED' - | 'NOT_FOUND' - | 'ERROR'; - -export default function useEventDetail() { - const [loading, setLoading] = useState(false); - const [event, setEvent] = useState(null); - const [registrations, setRegistrations] = useState([]); - - // 1. 일정 상세 정보 로드 핸들러 - const handleFetchDetail = useCallback( - async (id: string): Promise => { - setLoading(true); - try { - const [eventRes, guestsRes] = await Promise.all([ - getEventDetail(id), - getRegistrations(id), - ]); - setEvent(eventRes.data); - setRegistrations(guestsRes); - - if (eventRes.status === 202) return 'ADMIN'; - if (eventRes.status === 204) return 'REGISTERED'; - return 'GUEST'; - } catch (error: unknown) { - if (isAxiosError(error)) { - if (error.response?.status === 404) return 'NOT_FOUND'; - } - console.error('Fetch event detail failed:', error); - toast.error('일정 정보를 불러오지 못했습니다.'); - return 'ERROR'; - } finally { - setLoading(false); - } - }, - [] - ); - - // 2. 참여자 전체 명단 로드 핸들러 - const handleFetchRegistrations = useCallback(async (id: string) => { - setLoading(true); - try { - const data = await getRegistrations(id); - setRegistrations(data); - } catch (error: unknown) { - console.error('Fetch registrations error:', error); - toast.error('참여자 명단을 불러오지 못했습니다.'); - } finally { - setLoading(false); - } - }, []); - - // 3. 참여 신청 로직 핸들러 - const handleJoinEvent = async ( - id: string, - data: JoinEventRequest - ): Promise => { - setLoading(true); - try { - const { status } = await registerEvent(id, data); - return status === 200 || status === 201; - } catch (error: unknown) { - if (isAxiosError(error) && error.response?.status === 409) { - toast.error('이미 신청이 완료된 모임입니다.'); - } else { - toast.error('신청 처리 중 오류가 발생했습니다.'); - } - console.error('Join event error:', error); - return false; - } finally { - setLoading(false); - } - }; - - const confirmedCount = registrations.filter( - (r) => r.status === 'CONFIRMED' - ).length; - - return { - loading, - event, - registrations, - confirmedCount, - handleFetchDetail, - handleFetchRegistrations, - handleJoinEvent, - }; -} diff --git a/src/routes/Event.tsx b/src/routes/Event.tsx index 717ed94..7c6a5cb 100644 --- a/src/routes/Event.tsx +++ b/src/routes/Event.tsx @@ -1,9 +1,10 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router'; import { toast } from 'sonner'; import EventDetailContent from '../components/EventDetailContent'; import GuestSummaryList from '../components/GuestSummaryList'; -import useEventDetail from '../hooks/useEventDetail'; +import type { Events } from '../types/schema'; +// import useAuth from '../hooks/useAuth'; // shadcn UI 컴포넌트 import { @@ -73,64 +74,53 @@ const IconLink = () => ( ); export default function Event() { - // 데이터 하드코딩 - // const [schedule, setSchedule] = useState(null); - // const [displayGuests, setDisplayGuests] = useState< - // { name: string; img: string | undefined }[] - // >([]); - - // useEffect(() => { - // - // const mockEvent: Events = { - // id: Number(id) || 1, - // title: '제2회 기획 세미나', - // description: - // '일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 ...', - // location: '서울대', - // start_at: '2026-02-02T18:00:00Z', - // end_at: '2026-02-02T20:00:00Z', - // capacity: 10, - // waitlist_enabled: true, - // registration_deadline: '2026-02-02T17:00:00Z', - // created_by: 123, // 관리자 ID - // created_at: '2026-01-14T00:00:00Z', - // updated_at: '2026-01-14T00:00:00Z', - // }; - - // setSchedule(mockEvent); + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); - // setDisplayGuests([ - // { name: '이름1', img: undefined }, // profile_image가 null인 경우 - // { name: '이름2', img: 'https://via.placeholder.com/40' }, - // { name: '이름3', img: undefined }, - // { name: '이름4', img: undefined }, - // ]); - // }, [id]); + // 관리자 확인용 + // const { isAdmin, isLoggedIn } = useAuth(); - // if (!schedule) return null; + const [schedule, setSchedule] = useState(null); + const [displayGuests, setDisplayGuests] = useState< + { name: string; img: string | undefined }[] + >([]); - const { id } = useParams<{ id: string }>(); - const navigate = useNavigate(); - const { loading, event, registrations, confirmedCount, handleFetchDetail } = - useEventDetail(); + const joinLink = `${window.location.origin}/join/${id}`; - // 권한 확인 및 데이터 로드 useEffect(() => { - if (!id) return; + // 데이터 하드코딩 + const mockEvent: Events = { + id: Number(id) || 1, + title: '제2회 기획 세미나', + description: + '일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 ...', + location: '서울대', + start_at: '2026-02-02T18:00:00Z', + end_at: '2026-02-02T20:00:00Z', + capacity: 10, + waitlist_enabled: true, + registration_deadline: '2026-02-02T17:00:00Z', + created_by: 123, // 관리자 ID + created_at: '2026-01-14T00:00:00Z', + updated_at: '2026-01-14T00:00:00Z', + }; - const init = async () => { - const status = await handleFetchDetail(id); + // 관리자 확인 로직 + // if (!isLoggedIn || !isAdmin(mockData.ownerName)) { + // alert('접근 권한이 없습니다. 관리자만 접근 가능합니다.'); + // navigate('/'); // 혹은 로그인 페이지로 이동 + // return; + // } - // 관리자 페이지이므로 'ADMIN' 상태가 아니면 무조건 차단 - if (status !== 'ADMIN') { - alert('관리자만 접근 가능한 페이지입니다.'); - // 뒤로가기 시 다시 이 페이지로 오지 못하게 replace 사용 - navigate('/', { replace: true }); - } - }; + setSchedule(mockEvent); - init(); - }, [id, handleFetchDetail, navigate]); + setDisplayGuests([ + { name: '이름1', img: undefined }, // profile_image가 null인 경우 + { name: '이름2', img: 'https://via.placeholder.com/40' }, + { name: '이름3', img: undefined }, + { name: '이름4', img: undefined }, + ]); + }, [id]); const handleDelete = () => { // 삭제 API 필요 @@ -139,8 +129,6 @@ export default function Event() { navigate('/'); }; - const joinLink = `${window.location.origin}/join/${id}`; - const handleCopyLink = () => { navigator.clipboard.writeText(joinLink); toast.success('링크가 복사되었습니다!', { @@ -148,14 +136,7 @@ export default function Event() { }); }; - // 로딩 중이거나 권한 확인 전에는 화면을 비움 - if (loading || !event) { - return ( -
-
-
- ); - } + if (!schedule) return null; return (
@@ -171,7 +152,7 @@ export default function Event() {

- {event.title} + {schedule.title}

@@ -223,10 +204,7 @@ export default function Event() { {/* 2. 메인 콘텐츠*/}
{/* 일정 정보 */} - + {/* 모집 마감 및 링크 블록 */}
@@ -251,9 +229,9 @@ export default function Event() { {/* 참여자 명단 섹션 */}
diff --git a/src/routes/EventRegister.tsx b/src/routes/EventRegister.tsx index d2d5e85..98cc963 100644 --- a/src/routes/EventRegister.tsx +++ b/src/routes/EventRegister.tsx @@ -1,11 +1,10 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import type { JoinEventRequest } from '@/types/events'; import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router'; import { GOOGLE_AUTH_URL, KAKAO_AUTH_URL } from '../constants/auth'; -import useEventDetail from '../hooks/useEventDetail'; +import type { Events } from '../types/schema'; import { formatEventDate } from '../utils/date'; // 아이콘 컴포넌트 @@ -27,8 +26,9 @@ const IconChevronLeft = () => ( export default function EventRegister() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const { loading, event, confirmedCount, handleFetchDetail, handleJoinEvent } = - useEventDetail(); + + // 일정 데이터 상태 + const [schedule, setSchedule] = useState(null); // 폼 상태 관리 const [formData, setFormData] = useState({ @@ -37,40 +37,34 @@ export default function EventRegister() { }); useEffect(() => { - if (id) { - handleFetchDetail(id); - } - }, [id, handleFetchDetail]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!id) return; + // Event.tsx와 동일한 구성의 Mock 데이터 + const mockEvent: Events = { + id: Number(id) || 1, + title: '제2회 기획 세미나', + description: '일정 설명...', + location: '서울대', + start_at: '2026-02-02T18:00:00Z', + end_at: '2026-02-02T20:00:00Z', + capacity: 10, + waitlist_enabled: true, + registration_deadline: '2026-02-02T17:00:00Z', + created_by: 123, + created_at: '2026-01-14T00:00:00Z', + updated_at: '2026-01-14T00:00:00Z', + }; - // 간단한 유효성 검사 - if (!formData.name.trim() || !formData.email.trim()) { - alert('성함과 이메일을 모두 입력해주세요.'); - return; - } + setSchedule(mockEvent); + }, [id]); - const requestData: JoinEventRequest = { - guestName: formData.name, - guestEmail: formData.email, - }; + const handleJoin = (e: React.FormEvent) => { + e.preventDefault(); + console.info('신청 시도:', { eventId: id, ...formData }); - const success = await handleJoinEvent(id, requestData); - if (success) { - // 신청 성공 시 성공 페이지로 이동 - navigate(`/join/${id}/success`); - } + // 신청 완료 페이지로 이동 + navigate(`/join/${id}/success`); }; - if (loading || !event) { - return ( -
-
-
- ); - } + if (!schedule) return null; return (
@@ -86,7 +80,7 @@ export default function EventRegister() {

- {event.title} + {schedule.title}

@@ -96,10 +90,10 @@ export default function EventRegister() { {/* 일정 정보 */}

- 일시 {formatEventDate(event.startAt)} + 일시 {formatEventDate(schedule.start_at)}

- 장소 {event.location || '미정'} + 장소 {schedule.location || '미정'}

@@ -108,9 +102,9 @@ export default function EventRegister() { onClick={() => navigate('guests')} className="flex items-center text-lg font-bold group hover:opacity-70 transition-opacity" > - {event.capacity}명 중{' '} + {schedule.capacity}명 중{' '} - {confirmedCount}명 신청 + {/* 신청 인원 필드 필요 */} 8명 신청
@@ -120,7 +114,7 @@ export default function EventRegister() {
{/* 신청 폼 섹션 */} -
+

예약자 정보를 입력해 주세요 diff --git a/src/routes/EventRegisterSuccess.tsx b/src/routes/EventRegisterSuccess.tsx index 402461a..67a5fe7 100644 --- a/src/routes/EventRegisterSuccess.tsx +++ b/src/routes/EventRegisterSuccess.tsx @@ -1,8 +1,8 @@ import { Button } from '@/components/ui/button'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router'; import EventDetailContent from '../components/EventDetailContent'; -import useEventDetail from '../hooks/useEventDetail'; +import type { Events } from '../types/schema'; // shadcn UI 컴포넌트 import { @@ -36,22 +36,29 @@ const IconChevronLeft = () => ( export default function EventRegisterSuccess() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const { loading, event, confirmedCount, handleFetchDetail } = - useEventDetail(); + const [schedule, setSchedule] = useState(null); useEffect(() => { - if (id) { - handleFetchDetail(id); - } - }, [id, handleFetchDetail]); + // Event.tsx와 동일한 구성의 Mock 데이터 + const mockEvent: Events = { + id: Number(id) || 1, + title: '제2회 기획 세미나', + description: + '일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 ...', + location: '서울대', + start_at: '2026-02-02T18:00:00Z', + end_at: '2026-02-02T20:00:00Z', + capacity: 10, + waitlist_enabled: true, + registration_deadline: '2026-02-02T17:00:00Z', + created_by: 123, + created_at: '2026-01-14T00:00:00Z', + updated_at: '2026-01-14T00:00:00Z', + }; + setSchedule(mockEvent); + }, [id]); - if (loading || !event) { - return ( -
-
-
- ); - } + if (!schedule) return null; return (
@@ -77,15 +84,12 @@ export default function EventRegisterSuccess() { {/* 일정 정보 (왼쪽 정렬) */}

- {event.title} + {schedule.title}

{/* 일정 정보 */} - + {/* 취소 버튼 */} diff --git a/src/routes/Guests.tsx b/src/routes/Guests.tsx index b5763c1..8bb22f5 100644 --- a/src/routes/Guests.tsx +++ b/src/routes/Guests.tsx @@ -1,7 +1,6 @@ -import { useEffect } from 'react'; -import { useNavigate, useParams } from 'react-router'; +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router'; import { toast } from 'sonner'; -import useEventDetail from '../hooks/useEventDetail'; // shadcn UI 컴포넌트 import { @@ -34,96 +33,82 @@ const IconChevronLeft = () => ( ); -// interface GuestResponse { -// registration_id: number; -// name: string; -// email: string | null; -// profile_image: string | null; -// } +interface GuestResponse { + registration_id: number; + name: string; + email: string | null; + profile_image: string | null; +} export default function Guests() { - // const [guests, setGuests] = useState([]); - - // useEffect(() => { - // // 데이터 하드코딩 - // const mockGuests: GuestResponse[] = [ - // { - // registration_id: 1, - // name: '이준엽', - // email: 'jun411@snu.ac.kr', - // profile_image: 'https://github.com/shadcn.png', - // }, - // { - // registration_id: 2, - // name: '이름2', - // email: '이메일@example.com', - // profile_image: null, - // }, - // { - // registration_id: 3, - // name: '이름3', - // email: '이메일@example.com', - // profile_image: null, - // }, - // { - // registration_id: 4, - // name: '이름4', - // email: '이메일@example.com', - // profile_image: null, - // }, - // { - // registration_id: 5, - // name: '이름5', - // email: null, - // profile_image: null, - // }, - // { - // registration_id: 6, - // name: '이름6', - // email: '이메일@example.com', - // profile_image: null, - // }, - // { - // registration_id: 7, - // name: '이름7', - // email: '이메일@example.com', - // profile_image: null, - // }, - // { - // registration_id: 8, - // name: '이름8', - // email: '이메일@example.com', - // profile_image: null, - // }, - // ]; - - // setGuests(mockGuests); - // }, []); - - const { id } = useParams<{ id: string }>(); + // API로 불러올 registrations id + // const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const { loading, registrations, handleFetchRegistrations } = useEventDetail(); + + const [guests, setGuests] = useState([]); useEffect(() => { - if (id) { - handleFetchRegistrations(id); - } - }, [id, handleFetchRegistrations]); + // 데이터 하드코딩 + const mockGuests: GuestResponse[] = [ + { + registration_id: 1, + name: '이준엽', + email: 'jun411@snu.ac.kr', + profile_image: 'https://github.com/shadcn.png', + }, + { + registration_id: 2, + name: '이름2', + email: '이메일@example.com', + profile_image: null, + }, + { + registration_id: 3, + name: '이름3', + email: '이메일@example.com', + profile_image: null, + }, + { + registration_id: 4, + name: '이름4', + email: '이메일@example.com', + profile_image: null, + }, + { + registration_id: 5, + name: '이름5', + email: null, + profile_image: null, + }, + { + registration_id: 6, + name: '이름6', + email: '이메일@example.com', + profile_image: null, + }, + { + registration_id: 7, + name: '이름7', + email: '이메일@example.com', + profile_image: null, + }, + { + registration_id: 8, + name: '이름8', + email: '이메일@example.com', + profile_image: null, + }, + ]; + + setGuests(mockGuests); + }, []); - const handleCancelGuest = (name: string | null) => { + const handleCancelGuest = (regId: number, name: string) => { // 삭제 API 필요 + setGuests((prev) => prev.filter((g) => g.registration_id !== regId)); toast.success(`${name} 님의 참여가 취소되었습니다.`); }; - // 로딩 중이거나 데이터가 아직 없을 때 (리다이렉트 판단 전) 스피너나 빈 화면 표시 - if (loading || !registrations) { - return ( -
-
-
- ); - } - return (
{/* 1. 상단 네비게이션 */} @@ -138,33 +123,31 @@ export default function Guests() {

- 참여자 명단({registrations.length}) + 참여자 명단({guests.length})

{/* 2. 참여자 리스트 */}
- {registrations.map((registration) => ( + {guests.map((guest) => (
- + - {registration.guestName?.slice(0, 2)} + {guest.name.slice(0, 2)}
- {registration.guestName} + {guest.name} - {registration.guestEmail ? ( - - {registration.guestEmail} - + {guest.email ? ( + {guest.email} ) : null}
@@ -182,8 +165,7 @@ export default function Guests() { - {registration.guestName} 님의 신청을 - 취소하시겠습니까? + {guest.name} 님의 신청을 취소하시겠습니까? 취소 후 원복이 어렵습니다. 취소 메일이 참여자에게 @@ -193,7 +175,9 @@ export default function Guests() { 신청 유지하기 handleCancelGuest(registration.guestName)} + onClick={() => + handleCancelGuest(guest.registration_id, guest.name) + } className="bg-red-600 hover:bg-red-700" > 취소하기 diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index e87d199..24bd6ec 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -5,18 +5,38 @@ import { Link } from 'react-router'; const events = [ { - id: 0, - title: 'string', - description: 'string', - location: 'string', - startAt: 0, - endAt: 0, - capacity: 0, - waitlistEnabled: true, - registrationDeadline: 0, - createdBy: 0, - createdAt: 0, - updatedAt: 0, + id: 1, + title: '제2회 기획 세미나', + description: + '일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 ...', + location: '서울대', + start_at: '2026-02-02T18:00:00Z', + end_at: '2026-02-02T20:00:00Z', + guests: 12, + capacity: 10, + waitlist_enabled: true, + registration_start: '2026-01-20T09:00:00Z', + registration_deadline: '2026-02-02T17:00:00Z', + created_by: 123, + created_at: '2026-01-14T00:00:00Z', + updated_at: '2026-01-14T00:00:00Z', + }, + { + id: 2, + title: '2026 와커톤', + description: + '그동안 각자의 위치에서 몰두해 온 여러분들이 한자리에 모여 뜨거운 에너지를 나누고, 무박 2일간 개발에 집중하며 즐길 수 있는 행사입니다.', + location: '성수 엘리스랩 라운지', + start_at: '2026-01-01T18:00:00Z', + end_at: '2026-02-13T20:00:00Z', + guests: 6, + capacity: 10, + waitlist_enabled: true, + registration_start: '2025-02-01T09:00:00Z', + registration_deadline: '2026-02-02T17:00:00Z', + created_by: 124, + created_at: '2026-01-14T00:00:00Z', + updated_at: '2026-01-14T00:00:00Z', }, ]; diff --git a/src/routes/JoinEvent.tsx b/src/routes/JoinEvent.tsx index aa90944..beafed0 100644 --- a/src/routes/JoinEvent.tsx +++ b/src/routes/JoinEvent.tsx @@ -1,10 +1,9 @@ import { Button } from '@/components/ui/button'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router'; import EventDetailContent from '../components/EventDetailContent'; import GuestSummaryList from '../components/GuestSummaryList'; -import useAuth from '../hooks/useAuth'; -import useEventDetail from '../hooks/useEventDetail'; +import type { Events } from '../types/schema'; // SVG 아이콘 컴포넌트 const IconChevronLeft = () => ( @@ -25,77 +24,47 @@ const IconChevronLeft = () => ( export default function JoinEvent() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const { isLoggedIn } = useAuth(); - const { - loading, - event, - registrations, - confirmedCount, - handleFetchDetail, - handleJoinEvent, - } = useEventDetail(); + const [schedule, setSchedule] = useState(null); + const [displayGuests, setDisplayGuests] = useState< + { name: string; img: string | undefined }[] + >([]); - // 초기 데이터 로드 및 리다이렉션 로직 useEffect(() => { - if (!id) return; - - const init = async () => { - const status = await handleFetchDetail(id); - - // 백엔드 상태 코드 기반 분기 처리 - switch (status) { - case 'ADMIN': - // 관리자라면 관리자 전용 상세 페이지로 리다이렉트 - navigate(`/event/${id}`, { replace: true }); - break; - case 'REGISTERED': - // 이미 신청한 사람이라면 성공 페이지로 리다이렉트 - navigate(`/join/${id}/success`, { replace: true }); - break; - case 'NOT_FOUND': - navigate('/', { replace: true }); - break; - case 'ERROR': - // 에러 시 알림은 훅에서 toast로 처리하므로 메인으로만 이동 - navigate('/'); - break; - default: - // 'GUEST' (200 OK)인 경우에만 페이지에 머무름 - break; - } + // 데이터 하드코딩 + const mockEvent: Events = { + id: Number(id) || 1, + title: '제2회 기획 세미나', + description: + '일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 일정설명 ...', + location: '서울대', + start_at: '2026-02-02T18:00:00Z', + end_at: '2026-02-02T20:00:00Z', + capacity: 10, + waitlist_enabled: true, + registration_deadline: '2026-02-02T17:00:00Z', + created_by: 123, // 관리자 ID + created_at: '2026-01-14T00:00:00Z', + updated_at: '2026-01-14T00:00:00Z', }; - init(); - }, [id, handleFetchDetail, navigate]); - - // 신청하기 버튼 핸들러 - const onJoinClick = async () => { - if (!id) return; + // 관리자 확인 로직 + // if (!isLoggedIn || !isAdmin(mockData.ownerName)) { + // alert('접근 권한이 없습니다. 관리자만 접근 가능합니다.'); + // navigate('/'); // 혹은 로그인 페이지로 이동 + // return; + // } - // 로그인 되어 있지 않으면 참여 신청 페이지(폼 입력)로 이동 - if (!isLoggedIn) { - return navigate(`/join/${id}/register`); - } + setSchedule(mockEvent); - // 로그인 되어 있으면 즉시 API 호출 - const success = await handleJoinEvent(id, { - guestName: null, - guestEmail: null, - }); + setDisplayGuests([ + { name: '이름1', img: undefined }, // profile_image가 null인 경우 + { name: '이름2', img: 'https://via.placeholder.com/40' }, + { name: '이름3', img: undefined }, + { name: '이름4', img: undefined }, + ]); + }, [id]); - if (success) { - navigate(`/join/${id}/success`); - } - }; - - // 로딩 중이거나 데이터가 아직 없을 때 (리다이렉트 판단 전) 스피너나 빈 화면 표시 - if (loading || !event) { - return ( -
-
-
- ); - } + if (!schedule) return null; return (
@@ -111,7 +80,7 @@ export default function JoinEvent() {

- {event.title} + {schedule.title}

@@ -119,25 +88,18 @@ export default function JoinEvent() { {/* 메인 콘텐츠 */}
{/* 일정 정보 */} - + {/* 신청하기 버튼*/} - {/* 참여자 명단 섹션 */}
diff --git a/src/routes/Login.tsx b/src/routes/Login.tsx index 00d2252..9aeec8d 100644 --- a/src/routes/Login.tsx +++ b/src/routes/Login.tsx @@ -1,16 +1,16 @@ -import { GOOGLE_AUTH_URL, KAKAO_AUTH_URL } from '@/constants/auth'; -import useAuth from '@/hooks/useAuth'; import { useState } from 'react'; import { Link } from 'react-router'; +import { GOOGLE_AUTH_URL, KAKAO_AUTH_URL } from '../constants/auth'; +import useAuth from '../hooks/useAuth'; export default function Login() { - const [email, setEmail] = useState(''); + const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const { handleLogin } = useAuth(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - handleLogin({ email, password }); + handleLogin({ username, password }); }; return ( @@ -37,8 +37,8 @@ export default function Login() { required className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" placeholder="이메일 (아이디)" - value={email} - onChange={(e) => setEmail(e.target.value)} + value={username} + onChange={(e) => setUsername(e.target.value)} />
diff --git a/src/routes/RegisterForm.tsx b/src/routes/RegisterForm.tsx index a954f86..1e5f39f 100644 --- a/src/routes/RegisterForm.tsx +++ b/src/routes/RegisterForm.tsx @@ -8,7 +8,7 @@ export default function RegisterForm() { const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [photo, setPhoto] = useState(null); - const [profileImage, setProfileImage] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); const [showErrors, setShowErrors] = useState(false); const emailRef = useRef(null); @@ -22,12 +22,12 @@ export default function RegisterForm() { // 사진이 선택될 때마다 미리보기 URL 생성 useEffect(() => { if (!photo) { - setProfileImage(null); + setPreviewUrl(null); return; } const objectUrl = URL.createObjectURL(photo); - setProfileImage(objectUrl); + setPreviewUrl(objectUrl); // 컴포넌트 언마운트 시 메모리 누수 방지를 위해 URL 해제 return () => URL.revokeObjectURL(objectUrl); @@ -76,11 +76,7 @@ export default function RegisterForm() { return; } - if (profileImage) { - handleSignUp({ email, name, password, profileImage }); - } else { - handleSignUp({ email, name, password }); - } + handleSignUp({ username: email, password, name, photo }); }; const errorTextStyle = 'mt-1 text-xs text-red-500 font-medium'; @@ -95,9 +91,9 @@ export default function RegisterForm() {
- {profileImage ? ( + {previewUrl ? ( Preview diff --git a/src/types/auth.ts b/src/types/auth.ts index 8b89f12..d6e0718 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -1,43 +1,45 @@ -import type { User } from '@/types/schemas'; - -// ---------- /singup ---------- - -export interface SignUpRequest { +// 유저 기본 정보 +export interface User { + id: number; email: string; name: string; - password: string; - profileImage?: string; + profileImage: string; + createdAt: string; + updatedAt: string; } -export type SignUpResponse = User; - -// ---------- /logout ---------- +// 멤버 권한 타입 +type MemberRole = 'leader' | 'co-leader' | 'admin' | 'member'; -export interface LogoutResponse { - message: string; +// 내 정보 조회 시 함께 내려올 그룹 정보 +export interface UserGroup { + groupId: number; // group id + groupName: string; // group name + role: MemberRole; // member type } -// ---------- /login ---------- +// API 응답 공통 규격 +export interface AuthResponse { + user: User; + token: string; +} +// 로그인 요청 데이터 export interface LoginRequest { - email: string; - password: string; + username: string; // 이메일 + password: string; // 비밀번호 } -export interface LoginResponse { - token: string; +// 회원가입 요청 데이터 +export interface SignUpRequest { + username: string; + password: string; + name: string; + photo: File | null; // 회원가입 시에는 선택 사항일 수 있음 } -// ---------- /social ---------- - -type AuthProvider = 'google' | 'kakao'; - +// 소셜 로그인 요청 데이터 export interface SocialLoginRequest { - provider: AuthProvider; - code: string; -} - -export interface SocialLoginResponse { - token: string; - user: User; + provider: 'google' | 'kakao'; + code: string; // 플랫폼으로부터 받은 인가 코드 } diff --git a/src/types/events.ts b/src/types/events.ts deleted file mode 100644 index 60e960b..0000000 --- a/src/types/events.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Event, Guest } from '@/types/schemas'; - -// 일정 상세 응답 (GET /api/events/:id) -export type EventDetailResponse = Event; - -// 참여자 명단 (GET /api/events/:id/registrations) -export type RegistrationListResponse = Guest[]; - -// 참여 신청 (POST /api/events/:id/registrations) -export interface JoinEventRequest { - guestName: string | null; - guestEmail: string | null; -} - -export interface JoinEventResponse { - registration: Guest; - cancelToken: string; // 신청 취소용 토큰 -} diff --git a/src/types/schema.ts b/src/types/schema.ts new file mode 100644 index 0000000..dd9d9c7 --- /dev/null +++ b/src/types/schema.ts @@ -0,0 +1,60 @@ +// export interface UserIdentities { +// id: number; // PK / BIGINT +// user_id: number; // FK / BIGINT +// provider: string; // VARCHAR(50) +// provider_user_id: string; // VARCHAR(191) +// created_at: string; // DATETIME(6) +// } + +export interface Events { + id: number | null; // PK / BIGINT + title: string; // VARCHAR(255) + description: string | null; // TEXT + location: string | null; // VARCHAR(255) + start_at: string | null; // DATETIME(6) + end_at: string | null; // DATETIME(6) + capacity: number | null; // INT + waitlist_enabled: boolean; // BOOLEAN + registration_deadline: string | null; // DATETIME(6) + created_by: number; // FK / BIGINT + created_at: string; // DATETIME(6) + updated_at: string; // DATETIME(6) +} + +// export interface PendingUsers { +// id: number; // PK / BIGINT +// email: string; // VARCHAR(255) +// name: string; // VARCHAR(100) +// password_hash: string; // VARCHAR(255) +// verification_code: string; // VARCHAR(255) +// created_at: string; // TIMESTAMP +// expires_at: string; // TIMESTAMP +// } + +// export interface Users { +// id: number; // PK / BIGINT +// email: string; // VARCHAR(255) +// name: string; // VARCHAR(100) +// password_hash: string | null; // VARCHAR(255) +// profile_image: string | null; // VARCHAR(255) +// created_at: string; // DATETIME(6) +// updated_at: string; // DATETIME(6) +// } + +// export interface RegistrationTokens { +// id: number | null; // PK / BIGINT +// registration_id: number | null; // FK / BIGINT +// token_hash: string; // CHAR(64) +// purpose: string; // ENUM('CANCEL','CHANGE_VOTE') +// created_at: string; // DATETIME(6) +// } + +// export interface Registrations { +// id: number | null; // PK / BIGINT +// user_id: number; // FK / BIGINT +// event_id: number | null; // FK / BIGINT +// guest_name: string | null; // VARCHAR(100) +// guest_email: string | null; // VARCHAR(255) +// status: string; // ENUM('CONFIRMED','WAITING','CANCELED') +// created_at: string; // DATETIME(6) +// } diff --git a/src/types/schemas.ts b/src/types/schemas.ts deleted file mode 100644 index 5184f9b..0000000 --- a/src/types/schemas.ts +++ /dev/null @@ -1,31 +0,0 @@ -export interface User { - id: number; - email: string; - name: string; - profileImage?: string; -} - -export interface Event { - id: number; - title: string; - description?: string; - location?: string; - startAt?: number; // epoch milliseconds - endAt?: number; // epoch milliseconds - capacity?: number; - waitlistEnabled: boolean; - registrationDeadline?: number; // epoch milliseconds - createdBy: number; // 작성자 ID - createdAt?: number; - updatedAt?: number; -} - -export interface Guest { - id: number; - userId: number | null; // 회원인 경우 ID, 비회원이면 null - eventId: number; - guestName: string | null; // 비회원 이름 - guestEmail: string | null; // 비회원 이메일 - status: 'CONFIRMED' | 'WAITING' | 'CANCELED'; - createdAt: number; // 신청 일시 -} diff --git a/src/types/users.ts b/src/types/users.ts deleted file mode 100644 index d432963..0000000 --- a/src/types/users.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { User } from '@/types/schemas'; - -// ---------- /me ---------- -export type GetMeResponse = User; diff --git a/src/utils/date.ts b/src/utils/date.ts index 21f6a4c..3a8b1cb 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -1,14 +1,11 @@ export const formatEventDate = ( - dateString: number | string | null | undefined, // number 타입 추가 + dateString: string | null | undefined, fallback: string = '미정' ): string => { if (!dateString) return fallback; const date = new Date(dateString); - // 유효하지 않은 날짜 체크 - if (isNaN(date.getTime())) return fallback; - const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); @@ -23,9 +20,8 @@ export const formatEventDate = ( return `${year}/${month}/${day}(${weekDay}) ${hours}:${minutes}`; }; -export const getShortenedDate = (dateString: number | string): string => { +export const getShortenedDate = (dateString: string): string => { const date = new Date(dateString); - if (isNaN(date.getTime())) return '미정'; const month = String(date.getMonth() + 1); const day = String(date.getDate());