diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index 5871ff7f..63143b13 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -48,7 +48,6 @@ async function request( }, }; - const accessToken = localStorage.getItem('accessToken'); if (accessToken) { if (!config.headers) { @@ -57,7 +56,6 @@ async function request( config.headers['Authorization'] = `Bearer ${accessToken}`; } - if (method.toUpperCase() !== 'GET') { config.data = data; } diff --git a/frontend/src/api/common/commonTypes.ts b/frontend/src/api/common/commonTypes.ts new file mode 100644 index 00000000..e094754c --- /dev/null +++ b/frontend/src/api/common/commonTypes.ts @@ -0,0 +1,4 @@ +export interface OperatorInfo { + id: string; + name: string; +} diff --git a/frontend/src/api/schedule/scheduleAPI.ts b/frontend/src/api/schedule/scheduleAPI.ts index 87d7d0ac..82b7e383 100644 --- a/frontend/src/api/schedule/scheduleAPI.ts +++ b/frontend/src/api/schedule/scheduleAPI.ts @@ -8,15 +8,15 @@ import type { ScheduleUpdateRequest } from './scheduleTypes'; -export const findAvailableThemesByDate = async (date: string): Promise => { +export const fetchAvailableThemesByDate = async (date: string): Promise => { return await apiClient.get(`/schedules/themes?date=${date}`); }; -export const findSchedules = async (date: string, themeId: string): Promise => { +export const fetchSchedulesByDateAndTheme = async (date: string, themeId: string): Promise => { return await apiClient.get(`/schedules?date=${date}&themeId=${themeId}`); }; -export const findScheduleById = async (id: string): Promise => { +export const fetchScheduleById = async (id: string): Promise => { return await apiClient.get(`/schedules/${id}`); } diff --git a/frontend/src/api/schedule/scheduleTypes.ts b/frontend/src/api/schedule/scheduleTypes.ts index 9a08ac1e..c31c9fb9 100644 --- a/frontend/src/api/schedule/scheduleTypes.ts +++ b/frontend/src/api/schedule/scheduleTypes.ts @@ -1,3 +1,5 @@ +import type { OperatorInfo } from '@_api/common/commonTypes'; + export type ScheduleStatus = 'AVAILABLE' | 'HOLD' | 'RESERVED' | 'BLOCKED'; export const ScheduleStatus = { @@ -44,7 +46,7 @@ export interface ScheduleDetailRetrieveResponse { time: string; // "HH:mm" status: ScheduleStatus; createdAt: string; // or Date - createdBy: string; + createdBy: OperatorInfo; updatedAt: string; // or Date - updatedBy: string; + updatedBy: OperatorInfo; } diff --git a/frontend/src/api/theme/themeAPI.ts b/frontend/src/api/theme/themeAPI.ts index 7282709c..43fb1516 100644 --- a/frontend/src/api/theme/themeAPI.ts +++ b/frontend/src/api/theme/themeAPI.ts @@ -2,10 +2,12 @@ import apiClient from '@_api/apiClient'; import type { AdminThemeDetailRetrieveResponse, AdminThemeSummaryRetrieveListResponse, + SimpleActiveThemeListResponse, ThemeCreateRequest, ThemeCreateResponse, ThemeIdListResponse, ThemeInfoListResponse, + ThemeInfoResponse, ThemeUpdateRequest } from './themeTypes'; @@ -29,10 +31,14 @@ export const deleteTheme = async (id: string): Promise => { await apiClient.del(`/admin/themes/${id}`); }; -export const fetchUserThemes = async (): Promise => { - return await apiClient.get('/themes'); -}; - -export const findThemesByIds = async (request: ThemeIdListResponse): Promise => { +export const fetchThemesByIds = async (request: ThemeIdListResponse): Promise => { return await apiClient.post('/themes/batch', request); }; + +export const fetchThemeById = async (id: string): Promise => { + return await apiClient.get(`/themes/${id}`); +} + +export const fetchActiveThemes = async (): Promise => { + return await apiClient.get('/admin/themes/active'); +}; diff --git a/frontend/src/api/theme/themeTypes.ts b/frontend/src/api/theme/themeTypes.ts index ba28bc0d..0cc043ed 100644 --- a/frontend/src/api/theme/themeTypes.ts +++ b/frontend/src/api/theme/themeTypes.ts @@ -1,3 +1,5 @@ +import type { OperatorInfo } from '@_api/common/commonTypes'; + export interface AdminThemeDetailResponse { id: string; name: string; @@ -10,11 +12,11 @@ export interface AdminThemeDetailResponse { availableMinutes: number; expectedMinutesFrom: number; expectedMinutesTo: number; - isOpen: boolean; + isActive: boolean; createDate: string; // Assuming ISO string format updatedDate: string; // Assuming ISO string format - createdBy: string; - updatedBy: string; + createdBy: OperatorInfo; + updatedBy: OperatorInfo; } export interface ThemeCreateRequest { @@ -28,7 +30,7 @@ export interface ThemeCreateRequest { availableMinutes: number; expectedMinutesFrom: number; expectedMinutesTo: number; - isOpen: boolean; + isActive: boolean; } export interface ThemeCreateResponse { @@ -43,10 +45,11 @@ export interface ThemeUpdateRequest { price?: number; minParticipants?: number; maxParticipants?: number; + availableMinutes?: number; expectedMinutesFrom?: number; expectedMinutesTo?: number; - isOpen?: boolean; + isActive?: boolean; } export interface AdminThemeSummaryRetrieveResponse { @@ -54,7 +57,7 @@ export interface AdminThemeSummaryRetrieveResponse { name: string; difficulty: Difficulty; price: number; - isOpen: boolean; + isActive: boolean; } export interface AdminThemeSummaryRetrieveListResponse { @@ -73,11 +76,11 @@ export interface AdminThemeDetailRetrieveResponse { availableMinutes: number; expectedMinutesFrom: number; expectedMinutesTo: number; - isOpen: boolean; + isActive: boolean; createdAt: string; // LocalDateTime in Kotlin, map to string (ISO format) - createdBy: string; + createdBy: OperatorInfo; updatedAt: string; // LocalDateTime in Kotlin, map to string (ISO format) - updatedBy: string; + updatedBy: OperatorInfo; } export interface ThemeInfoResponse { @@ -102,18 +105,34 @@ export interface ThemeIdListResponse { themeIds: string[]; } -// @ts-ignore export enum Difficulty { - VERY_EASY = '매우 쉬움', - EASY = '쉬움', - NORMAL = '보통', - HARD = '어려움', - VERY_HARD = '매우 어려움', + VERY_EASY = 'VERY_EASY', + EASY = 'EASY', + NORMAL = 'NORMAL', + HARD = 'HARD', + VERY_HARD = 'VERY_HARD', } +export const DifficultyKoreanMap: Record = { + [Difficulty.VERY_EASY]: '매우 쉬움', + [Difficulty.EASY]: '쉬움', + [Difficulty.NORMAL]: '보통', + [Difficulty.HARD]: '어려움', + [Difficulty.VERY_HARD]: '매우 어려움', +}; + export function mapThemeResponse(res: any): ThemeInfoResponse { return { ...res, difficulty: Difficulty[res.difficulty as keyof typeof Difficulty], } +} + +export interface SimpleActiveThemeResponse { + id: string; + name: string; +} + +export interface SimpleActiveThemeListResponse { + themes: SimpleActiveThemeResponse[]; } \ No newline at end of file diff --git a/frontend/src/context/AdminAuthContext.tsx b/frontend/src/context/AdminAuthContext.tsx index 3cfe96d0..a62fe7f3 100644 --- a/frontend/src/context/AdminAuthContext.tsx +++ b/frontend/src/context/AdminAuthContext.tsx @@ -27,7 +27,7 @@ export const AdminAuthProvider: React.FC<{ children: ReactNode }> = ({ children useEffect(() => { try { - const token = localStorage.getItem('adminAccessToken'); + const token = localStorage.getItem('accessToken'); const storedName = localStorage.getItem('adminName'); const storedType = localStorage.getItem('adminType') as AdminType | null; const storedStoreId = localStorage.getItem('adminStoreId'); @@ -48,7 +48,7 @@ export const AdminAuthProvider: React.FC<{ children: ReactNode }> = ({ children const login = async (data: Omit) => { const response = await apiLogin(data); - localStorage.setItem('adminAccessToken', response.accessToken); + localStorage.setItem('accessToken', response.accessToken); localStorage.setItem('adminName', response.name); localStorage.setItem('adminType', response.type); if (response.storeId) { @@ -69,7 +69,7 @@ export const AdminAuthProvider: React.FC<{ children: ReactNode }> = ({ children try { await apiLogout(); } finally { - localStorage.removeItem('adminAccessToken'); + localStorage.removeItem('accessToken'); localStorage.removeItem('adminName'); localStorage.removeItem('adminType'); localStorage.removeItem('adminStoreId'); diff --git a/frontend/src/css/admin-schedule-page.css b/frontend/src/css/admin-schedule-page.css index 350d6c33..c4cb2775 100644 --- a/frontend/src/css/admin-schedule-page.css +++ b/frontend/src/css/admin-schedule-page.css @@ -211,4 +211,106 @@ th { .audit-body p strong { color: #212529; margin-right: 0.5rem; +} + +.theme-selector-button-group { + display: flex; + flex-direction: row !important; + align-items: flex-end; + gap: 0.5rem; +} + +.theme-selector-button-group .form-select { + flex-grow: 1; +} + +/* Modal Styles */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + background-color: #fff; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); + width: 90%; + max-width: 600px; + position: relative; +} + +.modal-close-btn { + position: absolute; + top: 1rem; + right: 1rem; + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: #888; +} + +.modal-title { + font-size: 1.75rem; + font-weight: bold; + margin-top: 0; + margin-bottom: 1.5rem; + text-align: center; +} + +.theme-modal-thumbnail { + width: 100%; + max-height: 300px; + object-fit: cover; + border-radius: 8px; + margin-bottom: 1.5rem; +} + +.theme-modal-description { + font-size: 1rem; + line-height: 1.6; + color: #555; + margin-bottom: 1.5rem; +} + +.theme-modal-info-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + background-color: #f9f9f9; + padding: 1rem; + border-radius: 8px; +} + +.info-item { + display: flex; + justify-content: space-between; + padding: 0.5rem; + border-bottom: 1px solid #eee; +} + +.info-item:last-child { + border-bottom: none; +} + +.info-item strong { + font-weight: 600; + color: #333; +} + +.info-item span { + color: #666; +} + +.theme-details-button { + align-self: center !important; } \ No newline at end of file diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index a5c22119..3d4461d5 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -2,7 +2,7 @@ import {fetchMostReservedThemeIds} from '@_api/reservation/reservationAPI'; import '@_css/home-page-v2.css'; import React, {useEffect, useState} from 'react'; import {useNavigate} from 'react-router-dom'; -import {findThemesByIds} from '@_api/theme/themeAPI'; +import {fetchThemesByIds} from '@_api/theme/themeAPI'; import {mapThemeResponse, type ThemeInfoResponse} from '@_api/theme/themeTypes'; const HomePage: React.FC = () => { @@ -25,7 +25,7 @@ const HomePage: React.FC = () => { if (themeIds === undefined) return; if (themeIds.length === 0) return; - const response = await findThemesByIds({ themeIds: themeIds }); + const response = await fetchThemesByIds({ themeIds: themeIds }); setRanking(response.themes.map(mapThemeResponse)); } catch (err) { console.error('Error fetching ranking:', err); diff --git a/frontend/src/pages/ReservationStep1Page.tsx b/frontend/src/pages/ReservationStep1Page.tsx index b73f607a..df648370 100644 --- a/frontend/src/pages/ReservationStep1Page.tsx +++ b/frontend/src/pages/ReservationStep1Page.tsx @@ -1,7 +1,7 @@ import {isLoginRequiredError} from '@_api/apiClient'; -import {findAvailableThemesByDate, findSchedules, holdSchedule} from '@_api/schedule/scheduleAPI'; +import {fetchAvailableThemesByDate, fetchSchedulesByDateAndTheme, holdSchedule} from '@_api/schedule/scheduleAPI'; import {type ScheduleRetrieveResponse, ScheduleStatus} from '@_api/schedule/scheduleTypes'; -import {findThemesByIds} from '@_api/theme/themeAPI'; +import {fetchThemesByIds} from '@_api/theme/themeAPI'; import {mapThemeResponse, type ThemeInfoResponse} from '@_api/theme/themeTypes'; import '@_css/reservation-v2-1.css'; import React, {useEffect, useState} from 'react'; @@ -35,13 +35,13 @@ const ReservationStep1Page: React.FC = () => { useEffect(() => { if (selectedDate) { const dateStr = selectedDate.toLocaleDateString('en-CA'); // yyyy-mm-dd - findAvailableThemesByDate(dateStr) + fetchAvailableThemesByDate(dateStr) .then(res => { console.log('Available themes response:', res); const themeIds: string[] = res.themeIds; console.log('Available theme IDs:', themeIds); if (themeIds.length > 0) { - return findThemesByIds({ themeIds }); + return fetchThemesByIds({ themeIds }); } else { return Promise.resolve({ themes: [] }); } @@ -69,7 +69,7 @@ const ReservationStep1Page: React.FC = () => { useEffect(() => { if (selectedDate && selectedTheme) { const dateStr = selectedDate.toLocaleDateString('en-CA'); - findSchedules(dateStr, selectedTheme.id) + fetchSchedulesByDateAndTheme(dateStr, selectedTheme.id) .then(res => { setSchedules(res.schedules); setSelectedSchedule(null); diff --git a/frontend/src/pages/admin/AdminNavbar.tsx b/frontend/src/pages/admin/AdminNavbar.tsx index a5563d6f..363ce561 100644 --- a/frontend/src/pages/admin/AdminNavbar.tsx +++ b/frontend/src/pages/admin/AdminNavbar.tsx @@ -1,10 +1,10 @@ -import {useAdminAuth} from '@_context/AdminAuthContext'; +import { useAdminAuth } from '@_context/AdminAuthContext'; import React from 'react'; -import {Link, useNavigate} from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import '@_css/navbar.css'; const AdminNavbar: React.FC = () => { - const { isAdmin, name, logout } = useAdminAuth(); + const { isAdmin, name, type, logout } = useAdminAuth(); const navigate = useNavigate(); const handleLogout = async (e: React.MouseEvent) => { @@ -21,7 +21,7 @@ const AdminNavbar: React.FC = () => {