diff --git a/build.gradle.kts b/build.gradle.kts index 8405fd6f..c6a792f2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -79,6 +79,9 @@ dependencies { // RestAssured testImplementation("io.rest-assured:rest-assured:5.5.5") testImplementation("io.rest-assured:kotlin-extensions:5.5.5") + + // etc + implementation("org.apache.poi:poi-ooxml:5.2.3") } tasks.withType { diff --git a/data/population.xlsx b/data/population.xlsx new file mode 100644 index 00000000..af8c2e8c Binary files /dev/null and b/data/population.xlsx differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 44f1900c..ed5c0139 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,10 +1,12 @@ -import {BrowserRouter as Router, Route, Routes} from 'react-router-dom'; -import AdminRoute from './components/AdminRoute'; +import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; import Layout from './components/Layout'; -import {AuthProvider} from './context/AuthContext'; +import { AdminAuthProvider } from './context/AdminAuthContext'; +import { AuthProvider } from './context/AuthContext'; import AdminLayout from './pages/admin/AdminLayout'; +import AdminLoginPage from './pages/admin/AdminLoginPage'; import AdminPage from './pages/admin/AdminPage'; import AdminSchedulePage from './pages/admin/AdminSchedulePage'; +import AdminStorePage from './pages/admin/AdminStorePage'; import AdminThemeEditPage from './pages/admin/AdminThemeEditPage'; import AdminThemePage from './pages/admin/AdminThemePage'; import HomePage from '@_pages/HomePage'; @@ -16,26 +18,28 @@ import ReservationStep2Page from '@_pages/ReservationStep2Page'; import ReservationSuccessPage from '@_pages/ReservationSuccessPage'; import SignupPage from '@_pages/SignupPage'; -const AdminRoutes = () => ( - - - } /> - } /> - } /> - } /> - - -); - function App() { return ( - - + + + } /> + + + } /> + } /> + } /> + } /> + } /> + + + } /> + + } /> @@ -57,4 +61,4 @@ function App() { ); } -export default App; \ No newline at end of file +export default App; diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index 5871ff7f..2db29e2d 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -1,5 +1,6 @@ import axios, {type AxiosError, type AxiosRequestConfig, type Method} from 'axios'; import JSONbig from 'json-bigint'; +import { PrincipalType } from './auth/authTypes'; // Create a JSONbig instance that stores big integers as strings const JSONbigString = JSONbig({ storeAsString: true }); @@ -38,7 +39,7 @@ async function request( method: Method, endpoint: string, data: object = {}, - isRequiredAuth: boolean = false + type: PrincipalType, ): Promise { const config: AxiosRequestConfig = { method, @@ -48,8 +49,9 @@ async function request( }, }; + const accessTokenKey = type === PrincipalType.ADMIN ? 'adminAccessToken' : 'accessToken'; + const accessToken = localStorage.getItem(accessTokenKey); - const accessToken = localStorage.getItem('accessToken'); if (accessToken) { if (!config.headers) { config.headers = {}; @@ -57,7 +59,6 @@ async function request( config.headers['Authorization'] = `Bearer ${accessToken}`; } - if (method.toUpperCase() !== 'GET') { config.data = data; } @@ -72,30 +73,50 @@ async function request( } } -async function get(endpoint: string, isRequiredAuth: boolean = false): Promise { - return request('GET', endpoint, {}, isRequiredAuth); +async function get(endpoint: string): Promise { + return request('GET', endpoint, {}, PrincipalType.USER); } -async function post(endpoint: string, data: object = {}, isRequiredAuth: boolean = false): Promise { - return request('POST', endpoint, data, isRequiredAuth); +async function adminGet(endpoint: string): Promise { + return request('GET', endpoint, {}, PrincipalType.ADMIN); } -async function put(endpoint: string, data: object = {}, isRequiredAuth: boolean = false): Promise { - return request('PUT', endpoint, data, isRequiredAuth); +async function post(endpoint: string, data: object = {}): Promise { + return request('POST', endpoint, data, PrincipalType.USER); } -async function patch(endpoint: string, data: object = {}, isRequiredAuth: boolean = false): Promise { - return request('PATCH', endpoint, data, isRequiredAuth); +async function adminPost(endpoint: string, data: object = {}): Promise { + return request('POST', endpoint, data, PrincipalType.ADMIN); } -async function del(endpoint: string, isRequiredAuth: boolean = false): Promise { - return request('DELETE', endpoint, {}, isRequiredAuth); +async function put(endpoint: string, data: object = {}): Promise { + return request('PUT', endpoint, data, PrincipalType.USER); +} + +async function adminPut(endpoint: string, data: object = {}): Promise { + return request('PUT', endpoint, data, PrincipalType.ADMIN); +} + +async function patch(endpoint: string, data: object = {}): Promise { + return request('PATCH', endpoint, data, PrincipalType.USER); +} + +async function adminPatch(endpoint: string, data: object = {}): Promise { + return request('PATCH', endpoint, data, PrincipalType.ADMIN); +} + +async function del(endpoint: string): Promise { + return request('DELETE', endpoint, {}, PrincipalType.USER); +} + +async function adminDel(endpoint: string): Promise { + return request('DELETE', endpoint, {}, PrincipalType.ADMIN); } export default { - get, - post, - put, - patch, - del + get, adminGet, + post, adminPost, + put, adminPut, + patch, adminPatch, + del, adminDel, }; diff --git a/frontend/src/api/auth/authAPI.ts b/frontend/src/api/auth/authAPI.ts index 93216199..141b3e45 100644 --- a/frontend/src/api/auth/authAPI.ts +++ b/frontend/src/api/auth/authAPI.ts @@ -1,19 +1,33 @@ import apiClient from '@_api/apiClient'; -import type {CurrentUserContext, LoginRequest, LoginSuccessResponse} from './authTypes'; +import { + type AdminLoginSuccessResponse, + type LoginRequest, + PrincipalType, + type UserLoginSuccessResponse, +} from './authTypes'; - -export const login = async (data: LoginRequest): Promise => { - const response = await apiClient.post('/auth/login', data, false); - localStorage.setItem('accessToken', response.accessToken); - - return response; +export const userLogin = async ( + data: Omit, +): Promise => { + return await apiClient.post( + '/auth/login', + { ...data, principalType: PrincipalType.USER }, + ); }; -export const checkLogin = async (): Promise => { - return await apiClient.get('/auth/login/check', true); +export const adminLogin = async ( + data: Omit, +): Promise => { + return await apiClient.adminPost( + '/auth/login', + { ...data, principalType: PrincipalType.ADMIN }, + ); }; export const logout = async (): Promise => { - await apiClient.post('/auth/logout', {}, true); - localStorage.removeItem('accessToken'); + await apiClient.post('/auth/logout', {}); }; + +export const adminLogout = async (): Promise => { + await apiClient.adminPost('/auth/logout', {}); +} diff --git a/frontend/src/api/auth/authTypes.ts b/frontend/src/api/auth/authTypes.ts index 426168c4..c67e1e92 100644 --- a/frontend/src/api/auth/authTypes.ts +++ b/frontend/src/api/auth/authTypes.ts @@ -5,6 +5,13 @@ export const PrincipalType = { export type PrincipalType = typeof PrincipalType[keyof typeof PrincipalType]; +export const AdminType = { + HQ: 'HQ', + STORE: 'STORE', +} as const; + +export type AdminType = typeof AdminType[keyof typeof AdminType]; + export interface LoginRequest { account: string, password: string; @@ -13,6 +20,15 @@ export interface LoginRequest { export interface LoginSuccessResponse { accessToken: string; + name: string; +} + +export interface UserLoginSuccessResponse extends LoginSuccessResponse { +} + +export interface AdminLoginSuccessResponse extends LoginSuccessResponse { + type: AdminType; + storeId: string | null; } export interface CurrentUserContext { diff --git a/frontend/src/api/common/commonTypes.ts b/frontend/src/api/common/commonTypes.ts new file mode 100644 index 00000000..babcdab3 --- /dev/null +++ b/frontend/src/api/common/commonTypes.ts @@ -0,0 +1,11 @@ +export interface OperatorInfo { + id: string; + name: string; +} + +export interface AuditInfo { + createdAt: string; + updatedAt: string; + createdBy: OperatorInfo; + updatedBy: OperatorInfo; +} \ No newline at end of file diff --git a/frontend/src/api/region/regionAPI.ts b/frontend/src/api/region/regionAPI.ts new file mode 100644 index 00000000..7927bbfe --- /dev/null +++ b/frontend/src/api/region/regionAPI.ts @@ -0,0 +1,14 @@ +import apiClient from "@_api/apiClient"; +import type { RegionCodeResponse, SidoListResponse, SigunguListResponse } from "./regionTypes"; + +export const fetchSidoList = async (): Promise => { + return await apiClient.get(`/regions/sido`); +}; + +export const fetchSigunguList = async (sidoCode: string): Promise => { + return await apiClient.get(`/regions/sigungu?sidoCode=${sidoCode}`); +} + +export const fetchRegionCode = async (sidoCode: string, sigunguCode: string): Promise => { + return await apiClient.get(`/regions/code?sidoCode=${sidoCode}&sigunguCode=${sigunguCode}`); +} diff --git a/frontend/src/api/region/regionTypes.ts b/frontend/src/api/region/regionTypes.ts new file mode 100644 index 00000000..7c8f181d --- /dev/null +++ b/frontend/src/api/region/regionTypes.ts @@ -0,0 +1,27 @@ +export interface SidoResponse { + code: string, + name: string, +} + +export interface SidoListResponse { + sidoList: SidoResponse[] +} + +export interface SigunguResponse { + code: string, + name: string, +} + +export interface SigunguListResponse { + sigunguList: SigunguResponse[] +} + +export interface RegionCodeResponse { + code: string +} + +export interface RegionInfoResponse { + code: string, + sidoName: string, + sigunguName: string, +} \ No newline at end of file diff --git a/frontend/src/api/reservation/reservationAPI.ts b/frontend/src/api/reservation/reservationAPI.ts index f2a80d5e..74f96d26 100644 --- a/frontend/src/api/reservation/reservationAPI.ts +++ b/frontend/src/api/reservation/reservationAPI.ts @@ -4,7 +4,7 @@ import type { PendingReservationCreateRequest, PendingReservationCreateResponse, ReservationDetailRetrieveResponse, - ReservationSummaryRetrieveListResponse + ReservationOverviewListResponse } from './reservationTypes'; export const createPendingReservation = async (request: PendingReservationCreateRequest): Promise => { @@ -17,11 +17,11 @@ export const confirmReservation = async (reservationId: string): Promise = export const cancelReservation = async (id: string, cancelReason: string): Promise => { - return await apiClient.post(`/reservations/${id}/cancel`, { cancelReason }, true); + return await apiClient.post(`/reservations/${id}/cancel`, { cancelReason }); }; -export const fetchSummaryByMember = async (): Promise => { - return await apiClient.get('/reservations/summary'); +export const fetchAllOverviewByUser = async (): Promise => { + return await apiClient.get('/reservations/overview'); } export const fetchDetailById = async (reservationId: string): Promise => { @@ -29,5 +29,5 @@ export const fetchDetailById = async (reservationId: string): Promise => { - return await apiClient.get(`/reservations/popular-themes?count=${count}`, false); + return await apiClient.get(`/reservations/popular-themes?count=${count}`); } \ No newline at end of file diff --git a/frontend/src/api/reservation/reservationTypes.ts b/frontend/src/api/reservation/reservationTypes.ts index 5c19d21a..6ffd2a72 100644 --- a/frontend/src/api/reservation/reservationTypes.ts +++ b/frontend/src/api/reservation/reservationTypes.ts @@ -1,6 +1,24 @@ import type {PaymentRetrieveResponse} from "@_api/payment/PaymentTypes"; import type {UserContactRetrieveResponse} from "@_api/user/userTypes"; +export interface ReservationData { + scheduleId: string; + store: { + id: string; + name: string; + } + theme: { + id: string; + name: string; + price: number; + minParticipants: number; + maxParticipants: number; + } + date: string; // "yyyy-MM-dd" + startFrom: string; // "HH:mm ~ HH:mm" + endAt: string; +} + export const ReservationStatus = { PENDING: 'PENDING', CONFIRMED: 'CONFIRMED', @@ -28,30 +46,38 @@ export interface PendingReservationCreateResponse { id: string } -export interface ReservationSummaryRetrieveResponse { +export interface ReservationOverviewResponse { id: string; + storeName: string; themeName: string; date: string; - startAt: string; + startFrom: string; + endAt: string; status: ReservationStatus; } -export interface ReservationSummaryRetrieveListResponse { - reservations: ReservationSummaryRetrieveResponse[]; +export interface ReservationOverviewListResponse { + reservations: ReservationOverviewResponse[]; +} + +export interface ReserverInfo { + name: string; + contact: string; + participantCount: number; + requirement: string; } export interface ReservationDetailRetrieveResponse { id: string; + reserver: ReserverInfo; user: UserContactRetrieveResponse; applicationDateTime: string; payment: PaymentRetrieveResponse; } export interface ReservationDetail { - id: string; - themeName: string; - date: string; - startAt: string; + overview: ReservationOverviewResponse; + reserver: ReserverInfo; user: UserContactRetrieveResponse; applicationDateTime: string; payment?: PaymentRetrieveResponse; @@ -59,4 +85,4 @@ export interface ReservationDetail { export interface MostReservedThemeIdListResponse { themeIds: string[]; -} \ No newline at end of file +} diff --git a/frontend/src/api/schedule/scheduleAPI.ts b/frontend/src/api/schedule/scheduleAPI.ts index 87d7d0ac..4ca28dc3 100644 --- a/frontend/src/api/schedule/scheduleAPI.ts +++ b/frontend/src/api/schedule/scheduleAPI.ts @@ -1,37 +1,49 @@ -import apiClient from '../apiClient'; -import type { - AvailableThemeIdListResponse, - ScheduleCreateRequest, - ScheduleCreateResponse, - ScheduleDetailRetrieveResponse, - ScheduleRetrieveListResponse, - ScheduleUpdateRequest -} from './scheduleTypes'; +import apiClient from "@_api/apiClient"; +import type { AdminScheduleSummaryListResponse, ScheduleCreateRequest, ScheduleCreateResponse, ScheduleStatus, ScheduleUpdateRequest, ScheduleWithThemeListResponse } from "./scheduleTypes"; +import type { AuditInfo } from "@_api/common/commonTypes"; -export const findAvailableThemesByDate = async (date: string): Promise => { - return await apiClient.get(`/schedules/themes?date=${date}`); -}; +// admin +export const fetchAdminSchedules = async (storeId: string, date?: string, themeId?: string): Promise => { + const queryParams: string[] = []; -export const findSchedules = async (date: string, themeId: string): Promise => { - return await apiClient.get(`/schedules?date=${date}&themeId=${themeId}`); -}; + if (date && date.trim() !== '') { + queryParams.push(`date=${date}`); + } + + if (themeId && themeId.trim() !== '') { + queryParams.push(`themeId=${themeId}`); + } + + // 기본 URL에 쿼리 파라미터 추가 + const baseUrl = `/admin/stores/${storeId}/schedules`; + const fullUrl = queryParams.length > 0 + ? `${baseUrl}?${queryParams.join('&')}` + : baseUrl; -export const findScheduleById = async (id: string): Promise => { - return await apiClient.get(`/schedules/${id}`); + return await apiClient.adminGet(fullUrl); } -export const createSchedule = async (request: ScheduleCreateRequest): Promise => { - return await apiClient.post('/schedules', request); +export const fetchScheduleAudit = async (scheduleId: string): Promise => { + return await apiClient.adminGet(`/admin/schedules/${scheduleId}/audits`); +} + +export const createSchedule = async (storeId: string, request: ScheduleCreateRequest): Promise => { + return await apiClient.adminPost(`/admin/stores/${storeId}/schedules`, request); }; export const updateSchedule = async (id: string, request: ScheduleUpdateRequest): Promise => { - await apiClient.patch(`/schedules/${id}`, request); + return await apiClient.adminPatch(`/admin/schedules/${id}`, request); }; export const deleteSchedule = async (id: string): Promise => { - await apiClient.del(`/schedules/${id}`); + return await apiClient.adminDel(`/admin/schedules/${id}`); }; +// public export const holdSchedule = async (id: string): Promise => { - await apiClient.patch(`/schedules/${id}/hold`, {}); + return await apiClient.post(`/schedules/${id}/hold`); +}; + +export const fetchSchedules = async (storeId: string, date: string): Promise => { + return await apiClient.get(`/stores/${storeId}/schedules?date=${date}`); }; diff --git a/frontend/src/api/schedule/scheduleTypes.ts b/frontend/src/api/schedule/scheduleTypes.ts index 9a08ac1e..0acf9f64 100644 --- a/frontend/src/api/schedule/scheduleTypes.ts +++ b/frontend/src/api/schedule/scheduleTypes.ts @@ -1,3 +1,5 @@ +import type { Difficulty } from '@_api/theme/themeTypes'; + export type ScheduleStatus = 'AVAILABLE' | 'HOLD' | 'RESERVED' | 'BLOCKED'; export const ScheduleStatus = { @@ -7,24 +9,11 @@ export const ScheduleStatus = { BLOCKED: 'BLOCKED' as ScheduleStatus, }; -export interface AvailableThemeIdListResponse { - themeIds: string[]; -} - -export interface ScheduleRetrieveResponse { - id: string; - time: string; // "HH:mm" - status: ScheduleStatus; -} - -export interface ScheduleRetrieveListResponse { - schedules: ScheduleRetrieveResponse[]; -} - +// Admin export interface ScheduleCreateRequest { - date: string; // "yyyy-MM-dd" - time: string; // "HH:mm" + date: string; themeId: string; + time: string; } export interface ScheduleCreateResponse { @@ -38,13 +27,29 @@ export interface ScheduleUpdateRequest { status?: ScheduleStatus; } -export interface ScheduleDetailRetrieveResponse { - id: string; - date: string; // "yyyy-MM-dd" - time: string; // "HH:mm" - status: ScheduleStatus; - createdAt: string; // or Date - createdBy: string; - updatedAt: string; // or Date - updatedBy: string; +export interface AdminScheduleSummaryResponse { + id: string, + themeName: string, + startFrom: string, + endAt: string, + status: ScheduleStatus, } + +export interface AdminScheduleSummaryListResponse { + schedules: AdminScheduleSummaryResponse[]; +} + +// Public +export interface ScheduleWithThemeResponse { + id: string, + startFrom: string, + endAt: string, + themeId: string, + themeName: string, + themeDifficulty: Difficulty, + status: ScheduleStatus +} + +export interface ScheduleWithThemeListResponse { + schedules: ScheduleWithThemeResponse[]; +} \ No newline at end of file diff --git a/frontend/src/api/store/storeAPI.ts b/frontend/src/api/store/storeAPI.ts new file mode 100644 index 00000000..80c9135c --- /dev/null +++ b/frontend/src/api/store/storeAPI.ts @@ -0,0 +1,48 @@ +import apiClient from '@_api/apiClient'; +import type { + SimpleStoreListResponse, + StoreCreateResponse, + StoreDetailResponse, + StoreInfoResponse, + StoreRegisterRequest, + UpdateStoreRequest +} from './storeTypes'; + +export const getStores = async (sidoCode?: string, sigunguCode?: string): Promise => { + const queryParams: string[] = []; + + if (sidoCode && sidoCode.trim() !== '') { + queryParams.push(`sidoCode=${sidoCode}`); + } + + if (sigunguCode && sigunguCode.trim() !== '') { + queryParams.push(`sigunguCode=${sigunguCode}`); + } + + const baseUrl = `/stores`; + const fullUrl = queryParams.length > 0 + ? `${baseUrl}?${queryParams.join('&')}` + : baseUrl; + + return await apiClient.get(fullUrl); +}; + +export const getStoreInfo = async (id: string): Promise => { + return await apiClient.get(`/stores/${id}`); +} + +export const getStoreDetail = async (id: string): Promise => { + return await apiClient.adminGet(`/admin/stores/${id}/detail`); +}; + +export const createStore = async (request: StoreRegisterRequest): Promise => { + return await apiClient.adminPost('/admin/stores', request); +}; + +export const updateStore = async (id: string, request: UpdateStoreRequest): Promise => { + await apiClient.adminPatch(`/admin/stores/${id}`, request); +}; + +export const deleteStore = async (id: string): Promise => { + await apiClient.adminPost(`/admin/stores/${id}/disable`, {}); +}; diff --git a/frontend/src/api/store/storeTypes.ts b/frontend/src/api/store/storeTypes.ts new file mode 100644 index 00000000..376549c4 --- /dev/null +++ b/frontend/src/api/store/storeTypes.ts @@ -0,0 +1,48 @@ +import {type AuditInfo} from '@_api/common/commonTypes'; +import type {RegionInfoResponse} from '@_api/region/regionTypes'; + +export interface SimpleStoreResponse { + id: string; + name: string; +} + +export interface SimpleStoreListResponse { + stores: SimpleStoreResponse[]; +} + +export interface StoreDetailResponse { + id: string; + name: string; + address: string; + contact: string; + businessRegNum: string; + region: RegionInfoResponse; + audit: AuditInfo; +} + +export interface StoreRegisterRequest { + name: string; + address: string; + contact: string; + businessRegNum: string; + regionCode: string; +} + +export interface UpdateStoreRequest { + name?: string; + address?: string; + contact?: string; + regionCode?: string; +} + +export interface StoreInfoResponse { + id: string; + name: string; + address: string; + contact: string; + businessRegNum: string; +} + +export interface StoreCreateResponse { + id: string; +} diff --git a/frontend/src/api/theme/themeAPI.ts b/frontend/src/api/theme/themeAPI.ts index 3db475d2..f0f6bdf9 100644 --- a/frontend/src/api/theme/themeAPI.ts +++ b/frontend/src/api/theme/themeAPI.ts @@ -1,38 +1,44 @@ import apiClient from '@_api/apiClient'; import type { - AdminThemeDetailRetrieveResponse, - AdminThemeSummaryRetrieveListResponse, + AdminThemeDetailResponse, + AdminThemeSummaryListResponse, + SimpleActiveThemeListResponse, ThemeCreateRequest, ThemeCreateResponse, ThemeIdListResponse, ThemeInfoListResponse, + ThemeInfoResponse, ThemeUpdateRequest } from './themeTypes'; -export const fetchAdminThemes = async (): Promise => { - return await apiClient.get('/admin/themes'); +export const fetchAdminThemes = async (): Promise => { + return await apiClient.adminGet('/admin/themes'); }; -export const fetchAdminThemeDetail = async (id: string): Promise => { - return await apiClient.get(`/admin/themes/${id}`); +export const fetchAdminThemeDetail = async (id: string): Promise => { + return await apiClient.adminGet(`/admin/themes/${id}`); }; export const createTheme = async (themeData: ThemeCreateRequest): Promise => { - return await apiClient.post('/admin/themes', themeData); + return await apiClient.adminPost('/admin/themes', themeData); }; export const updateTheme = async (id: string, themeData: ThemeUpdateRequest): Promise => { - await apiClient.patch(`/admin/themes/${id}`, themeData); + await apiClient.adminPatch(`/admin/themes/${id}`, themeData); }; export const deleteTheme = async (id: string): Promise => { - await apiClient.del(`/admin/themes/${id}`); + await apiClient.adminDel(`/admin/themes/${id}`); }; -export const fetchUserThemes = async (): Promise => { - return await apiClient.get('/themes'); +export const fetchActiveThemes = async (): Promise => { + return await apiClient.adminGet('/admin/themes/active'); }; -export const findThemesByIds = async (request: ThemeIdListResponse): Promise => { - return await apiClient.post('/themes/retrieve', request); +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}`); +} diff --git a/frontend/src/api/theme/themeTypes.ts b/frontend/src/api/theme/themeTypes.ts index ba28bc0d..2b56d825 100644 --- a/frontend/src/api/theme/themeTypes.ts +++ b/frontend/src/api/theme/themeTypes.ts @@ -1,20 +1,9 @@ +import type { AuditInfo } from '@_api/common/commonTypes'; + export interface AdminThemeDetailResponse { - id: string; - name: string; - description: string; - thumbnailUrl: string; - difficulty: Difficulty; - price: number; - minParticipants: number; - maxParticipants: number; - availableMinutes: number; - expectedMinutesFrom: number; - expectedMinutesTo: number; - isOpen: boolean; - createDate: string; // Assuming ISO string format - updatedDate: string; // Assuming ISO string format - createdBy: string; - updatedBy: string; + theme: ThemeInfoResponse; + isActive: boolean; + audit: AuditInfo } export interface ThemeCreateRequest { @@ -28,7 +17,7 @@ export interface ThemeCreateRequest { availableMinutes: number; expectedMinutesFrom: number; expectedMinutesTo: number; - isOpen: boolean; + isActive: boolean; } export interface ThemeCreateResponse { @@ -46,38 +35,19 @@ export interface ThemeUpdateRequest { availableMinutes?: number; expectedMinutesFrom?: number; expectedMinutesTo?: number; - isOpen?: boolean; + isActive?: boolean; } -export interface AdminThemeSummaryRetrieveResponse { +export interface AdminThemeSummaryResponse { id: string; name: string; difficulty: Difficulty; price: number; - isOpen: boolean; + isActive: boolean; } -export interface AdminThemeSummaryRetrieveListResponse { - themes: AdminThemeSummaryRetrieveResponse[]; -} - -export interface AdminThemeDetailRetrieveResponse { - id: string; - name: string; - description: string; - thumbnailUrl: string; - difficulty: Difficulty; - price: number; - minParticipants: number; - maxParticipants: number; - availableMinutes: number; - expectedMinutesFrom: number; - expectedMinutesTo: number; - isOpen: boolean; - createdAt: string; // LocalDateTime in Kotlin, map to string (ISO format) - createdBy: string; - updatedAt: string; // LocalDateTime in Kotlin, map to string (ISO format) - updatedBy: string; +export interface AdminThemeSummaryListResponse { + themes: AdminThemeSummaryResponse[]; } export interface ThemeInfoResponse { @@ -102,18 +72,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], } -} \ No newline at end of file +} + +export interface SimpleActiveThemeResponse { + id: string; + name: string; +} + +export interface SimpleActiveThemeListResponse { + themes: SimpleActiveThemeResponse[]; +} diff --git a/frontend/src/api/user/userAPI.ts b/frontend/src/api/user/userAPI.ts index 84a2422d..67236d75 100644 --- a/frontend/src/api/user/userAPI.ts +++ b/frontend/src/api/user/userAPI.ts @@ -2,9 +2,9 @@ import apiClient from "@_api/apiClient"; import type {UserContactRetrieveResponse, UserCreateRequest, UserCreateResponse} from "./userTypes"; export const signup = async (data: UserCreateRequest): Promise => { - return await apiClient.post('/users', data, false); + return await apiClient.post('/users', data); }; export const fetchContact = async (): Promise => { - return await apiClient.get('/users/contact', true); + return await apiClient.get('/users/contact'); } diff --git a/frontend/src/context/AdminAuthContext.tsx b/frontend/src/context/AdminAuthContext.tsx new file mode 100644 index 00000000..ba6c7cd1 --- /dev/null +++ b/frontend/src/context/AdminAuthContext.tsx @@ -0,0 +1,96 @@ +import { adminLogin as apiLogin, adminLogout as apiLogout } from '@_api/auth/authAPI'; +import { + type AdminLoginSuccessResponse, + type AdminType, + type LoginRequest, +} from '@_api/auth/authTypes'; +import React, { createContext, type ReactNode, useContext, useEffect, useState } from 'react'; + +interface AdminAuthContextType { + isAdmin: boolean; + name: string | null; + type: AdminType | null; + storeId: string | null; + loading: boolean; + login: (data: Omit) => Promise; + logout: () => Promise; +} + +const AdminAuthContext = createContext(undefined); + +export const AdminAuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [isAdmin, setIsAdmin] = useState(false); + const [name, setName] = useState(null); + const [type, setType] = useState(null); + const [storeId, setStoreId] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + try { + const token = localStorage.getItem('adminAccessToken'); + const storedName = localStorage.getItem('adminName'); + const storedType = localStorage.getItem('adminType') as AdminType | null; + const storedStoreId = localStorage.getItem('adminStoreId'); + + if (token && storedName && storedType) { + setIsAdmin(true); + setName(storedName); + setType(storedType); + setStoreId(storedStoreId ? storedStoreId : null); + } + } catch (error) { + console.error("Failed to load admin auth state from storage", error); + } finally { + setLoading(false); + } + }, []); + + const login = async (data: Omit) => { + const response = await apiLogin(data); + + localStorage.setItem('adminAccessToken', response.accessToken); + localStorage.setItem('adminName', response.name); + localStorage.setItem('adminType', response.type); + if (response.storeId) { + localStorage.setItem('adminStoreId', response.storeId.toString()); + } else { + localStorage.removeItem('adminStoreId'); + } + + setIsAdmin(true); + setName(response.name); + setType(response.type); + setStoreId(response.storeId); + + return response; + }; + + const logout = async () => { + try { + await apiLogout(); + } finally { + localStorage.removeItem('adminAccessToken'); + localStorage.removeItem('adminName'); + localStorage.removeItem('adminType'); + localStorage.removeItem('adminStoreId'); + setIsAdmin(false); + setName(null); + setType(null); + setStoreId(null); + } + }; + + return ( + + {children} + + ); +}; + +export const useAdminAuth = (): AdminAuthContextType => { + const context = useContext(AdminAuthContext); + if (!context) { + throw new Error('useAdminAuth must be used within an AdminAuthProvider'); + } + return context; +}; diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx index e220f436..0acfca2f 100644 --- a/frontend/src/context/AuthContext.tsx +++ b/frontend/src/context/AuthContext.tsx @@ -1,15 +1,13 @@ -import {checkLogin as apiCheckLogin, login as apiLogin, logout as apiLogout} from '@_api/auth/authAPI'; -import {type LoginRequest, type LoginSuccessResponse, PrincipalType} from '@_api/auth/authTypes'; -import React, {createContext, type ReactNode, useContext, useEffect, useState} from 'react'; +import { logout as apiLogout, userLogin as apiLogin } from '@_api/auth/authAPI'; +import { type LoginRequest, type UserLoginSuccessResponse } from '@_api/auth/authTypes'; +import React, { createContext, type ReactNode, useContext, useEffect, useState } from 'react'; interface AuthContextType { loggedIn: boolean; userName: string | null; - type: PrincipalType | null; - loading: boolean; - login: (data: LoginRequest) => Promise; + loading: boolean; + login: (data: Omit) => Promise; logout: () => Promise; - checkLogin: () => Promise; } const AuthContext = createContext(undefined); @@ -17,33 +15,33 @@ const AuthContext = createContext(undefined); export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [loggedIn, setLoggedIn] = useState(false); const [userName, setUserName] = useState(null); - const [type, setType] = useState(null); - const [loading, setLoading] = useState(true); // Add loading state - - const checkLogin = async () => { - try { - const response = await apiCheckLogin(); - setLoggedIn(true); - setUserName(response.name); - setType(response.type); - } catch (error) { - setLoggedIn(false); - setUserName(null); - setType(null); - localStorage.removeItem('accessToken'); - } finally { - setLoading(false); // Set loading to false after check is complete - } - }; + const [loading, setLoading] = useState(true); useEffect(() => { - checkLogin(); + try { + const token = localStorage.getItem('accessToken'); + const storedUserName = localStorage.getItem('userName'); + + if (token && storedUserName) { + setLoggedIn(true); + setUserName(storedUserName); + } + } catch (error) { + console.error("Failed to load user auth state from storage", error); + } finally { + setLoading(false); + } }, []); - const login = async (data: LoginRequest) => { - const response = await apiLogin({ ...data }); + const login = async (data: Omit) => { + const response = await apiLogin(data); + + localStorage.setItem('accessToken', response.accessToken); + localStorage.setItem('userName', response.name); + setLoggedIn(true); - setType(data.principalType); + setUserName(response.name); + return response; }; @@ -51,15 +49,15 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => try { await apiLogout(); } finally { + localStorage.removeItem('accessToken'); + localStorage.removeItem('userName'); setLoggedIn(false); setUserName(null); - setType(null); - localStorage.removeItem('accessToken'); } }; return ( - + {children} ); diff --git a/frontend/src/css/admin-schedule-page.css b/frontend/src/css/admin-schedule-page.css index 350d6c33..e559a855 100644 --- a/frontend/src/css/admin-schedule-page.css +++ b/frontend/src/css/admin-schedule-page.css @@ -1,11 +1,13 @@ +/* New CSS content */ .admin-schedule-container { padding: 2rem; max-width: 1200px; margin: 0 auto; + font-size: 0.95rem; /* Slightly smaller base font */ } .page-title { - font-size: 2rem; + font-size: 1.8rem; /* smaller */ font-weight: bold; margin-bottom: 2rem; text-align: center; @@ -18,7 +20,7 @@ padding: 1.5rem; background-color: #f9f9f9; border-radius: 8px; - align-items: center; + align-items: flex-end; /* Align to bottom */ } .schedule-controls .form-group { @@ -26,18 +28,29 @@ flex-direction: column; } +/* Width adjustments */ +.schedule-controls .store-selector-group, +.schedule-controls .date-selector-group { + flex: 1 1 180px; +} + +.schedule-controls .theme-selector-group { + flex: 2 1 300px; +} + + .schedule-controls .form-label { - font-size: 0.9rem; + font-size: 0.85rem; /* smaller */ margin-bottom: 0.5rem; color: #555; } .schedule-controls .form-input, .schedule-controls .form-select { - padding: 0.75rem; + padding: 0.6rem; /* smaller */ border: 1px solid #ccc; border-radius: 4px; - font-size: 1rem; + font-size: 0.9rem; /* smaller */ } .section-card { @@ -63,10 +76,11 @@ table { } th, td { - padding: 1rem; + padding: 0.8rem; /* smaller */ text-align: left; border-bottom: 1px solid #eee; vertical-align: middle; + font-size: 0.9rem; /* smaller */ } th { @@ -75,11 +89,11 @@ th { } .btn { - padding: 0.5rem 1rem; + padding: 0.4rem 0.8rem; /* smaller */ border: none; border-radius: 4px; cursor: pointer; - font-size: 0.9rem; + font-size: 0.85rem; /* smaller */ transition: background-color 0.2s; white-space: nowrap; } @@ -174,8 +188,8 @@ th { font-size: 1rem; border: 1px solid #dee2e6; border-radius: 4px; - height: 3rem; - box-sizing: border-box; /* Ensures padding/border are included in height */ + height: auto; /* remove fixed height */ + box-sizing: border-box; } .details-form-container .button-group { @@ -190,7 +204,7 @@ th { border: 1px solid #dee2e6; border-radius: 8px; background-color: #fff; - margin-bottom: 1.5rem; /* Add margin to separate from buttons */ + margin-bottom: 1.5rem; } .audit-title { @@ -211,4 +225,96 @@ th { .audit-body p strong { color: #212529; margin-right: 0.5rem; -} \ No newline at end of file +} + +.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: #ffffff !important; + padding: 2rem !important; + border-radius: 8px !important; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3) !important; + width: 90% !important; + max-width: 600px !important; + position: relative !important; +} + +.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-details-button { + white-space: nowrap; +} + +.view-mode-buttons { + justify-content: flex-end; +} + +/* Added for modal info alignment */ +.modal-info-grid p { + display: flex; + align-items: flex-start; + margin: 0.6rem 0; + line-height: 1.5; +} +.modal-info-grid p strong { + flex: 0 0 130px; /* fixed width for labels */ + font-weight: 600; +} +.modal-info-grid p span { + flex: 1; +} diff --git a/frontend/src/css/admin-store-page.css b/frontend/src/css/admin-store-page.css new file mode 100644 index 00000000..f8d5c0a4 --- /dev/null +++ b/frontend/src/css/admin-store-page.css @@ -0,0 +1,207 @@ +/* /src/css/admin-store-page.css */ +.admin-store-container { + max-width: 1400px; + margin: 40px auto; + padding: 40px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background-color: #f4f6f8; + border-radius: 16px; +} + +.admin-store-container .page-title { + font-size: 32px; + font-weight: 700; + color: #333d4b; + margin-bottom: 30px; + text-align: center; +} + +.filter-controls { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; + padding: 1.5rem; + background-color: #f9f9f9; + border-radius: 8px; +} + +.filter-controls .form-group { + flex: 1; +} + +.section-card { + background-color: #ffffff; + border-radius: 12px; + padding: 24px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} + +.table-header { + display: flex; + justify-content: flex-end; + margin-bottom: 20px; +} + +.table-container table { + width: 100%; + border-collapse: collapse; + font-size: 15px; +} + +.table-container th, +.table-container td { + padding: 12px 16px; + text-align: left; + border-bottom: 1px solid #e5e8eb; + vertical-align: middle; +} + +.table-container th { + background-color: #f9fafb; + color: #505a67; + font-weight: 600; +} + +.table-container tr:last-child td { + border-bottom: none; +} + +.table-container tr:hover { + background-color: #f4f6f8; +} + +.form-input, .form-select, .form-textarea { + width: 100%; + padding: 10px 12px; + font-size: 15px; + border: 1px solid #E5E8EB; + border-radius: 8px; + box-sizing: border-box; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.form-input:focus, .form-select:focus, .form-textarea:focus { + outline: none; + border-color: #3182F6; + box-shadow: 0 0 0 3px rgba(49, 130, 246, 0.2); +} + +.btn { + padding: 8px 16px; + font-size: 15px; + font-weight: 600; + border-radius: 8px; + border: none; + cursor: pointer; + transition: background-color 0.2s; +} + +.btn-primary { + background-color: #3182F6; + color: #ffffff; +} + +.btn-primary:hover { + background-color: #1B64DA; +} + +.btn-secondary { + background-color: #F2F4F6; + color: #4E5968; +} + +.btn-secondary:hover { + background-color: #E5E8EB; +} + +.btn-danger { + background-color: #e53e3e; + color: white; +} + +.btn-danger:hover { + background-color: #c53030; +} + +.action-buttons { + display: flex; + gap: 0.5rem; +} + +.details-row td { + padding: 0; + background-color: #f8f9fa; +} + +.details-container { + padding: 1.5rem; +} + +.details-form-card { + background-color: #fff; + padding: 1.5rem; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); + margin-bottom: 1.5rem; +} + +.form-row { + display: flex; + gap: 1.5rem; + margin-bottom: 1rem; +} + +.form-group { + flex: 1; +} + +.form-label { + display: block; + margin-bottom: 8px; + font-size: 14px; + font-weight: 600; + color: #4E5968; +} + +.button-group { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 1.5rem; +} + +.audit-info { + padding: 1.5rem; + border: 1px solid #dee2e6; + border-radius: 8px; + background-color: #fff; + margin-bottom: 1.5rem; +} + +.audit-title { + font-size: 1.1rem; + font-weight: 600; + color: #343a40; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid #dee2e6; +} + +.audit-body p { + margin: 0.5rem 0; + font-size: 0.9rem; + color: #495057; +} + +.audit-body p strong { + color: #212529; + margin-right: 0.5rem; +} + +.add-store-form { + padding: 1.5rem; + background-color: #fdfdff; + border: 1px solid #e5e8eb; + border-radius: 8px; + margin-bottom: 2rem; +} diff --git a/frontend/src/css/home-page-v2.css b/frontend/src/css/home-page-v2.css index 2728cb68..6df4daae 100644 --- a/frontend/src/css/home-page-v2.css +++ b/frontend/src/css/home-page-v2.css @@ -81,15 +81,15 @@ } .theme-modal-content { - background-color: #ffffff; - padding: 30px; - border-radius: 16px; - width: 90%; - max-width: 600px; - box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2); - display: flex; - flex-direction: column; - gap: 20px; + background-color: #ffffff !important; + padding: 30px !important; + border-radius: 16px !important; + width: 90% !important; + max-width: 600px !important; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2) !important; + display: flex !important; + flex-direction: column !important; + gap: 20px !important; } .modal-thumbnail { @@ -163,3 +163,18 @@ .modal-button.close:hover { background-color: #5a6268; } + +/* Added for modal info alignment */ +.modal-info-grid p { + display: flex; + align-items: flex-start; + margin: 0.6rem 0; + line-height: 1.5; +} +.modal-info-grid p strong { + flex: 0 0 130px; /* fixed width for labels */ + font-weight: 600; +} +.modal-info-grid p span { + flex: 1; +} diff --git a/frontend/src/css/my-reservation-v2.css b/frontend/src/css/my-reservation-v2.css index b6fa89b4..afaa66ea 100644 --- a/frontend/src/css/my-reservation-v2.css +++ b/frontend/src/css/my-reservation-v2.css @@ -49,10 +49,24 @@ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } +.summary-subdetails-v2 { + display: flex; + flex-direction: column; + margin: 0px; + gap: 0px; +} + +.summary-store-name-v2 { + font-size: 16px; + font-weight: bold; + color: #505a67; + margin: 0 0 5px 0; +} + .summary-details-v2 { display: flex; flex-direction: column; - gap: 4px; + gap: 10px; } .summary-theme-name-v2 { @@ -65,15 +79,15 @@ .summary-datetime-v2 { font-size: 16px; color: #505a67; - margin: 0; + margin-bottom: 5px; } /* --- Status Badge --- */ .card-status-badge { position: absolute; - top: 15px; - right: 15px; - padding: 4px 10px; + top: 30px; + right: 10px; + padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 700; @@ -177,16 +191,16 @@ } .modal-content-v2 { - background: #ffffff; - padding: 30px; - border-radius: 16px; - width: 90%; - max-width: 500px; - position: relative; - box-shadow: 0 5px 15px rgba(0,0,0,0.3); - animation: slide-up 0.3s ease-out; - max-height: 90vh; /* Prevent modal from being too tall */ - overflow-y: auto; /* Allow scrolling for long content */ + background: #ffffff !important; + padding: 30px !important; + border-radius: 16px !important; + width: 90% !important; + max-width: 500px !important; + position: relative !important; + box-shadow: 0 5px 15px rgba(0,0,0,0.3) !important; + animation: slide-up 0.3s ease-out !important; + max-height: 90vh !important; /* Prevent modal from being too tall */ + overflow-y: auto !important; /* Allow scrolling for long content */ } @keyframes slide-up { @@ -240,13 +254,6 @@ color: #505a67; } -.modal-section-v2 p strong { - color: #333d4b; - font-weight: 600; - min-width: 100px; - display: inline-block; -} - .cancellation-section-v2 { background-color: #fcf2f2; padding: 15px; @@ -346,3 +353,18 @@ border-color: #007bff; box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); } + +/* Added for modal info alignment */ +.modal-info-grid p { + display: flex; + align-items: flex-start; + margin: 0.6rem 0; + line-height: 1.5; +} +.modal-info-grid p strong { + flex: 0 0 130px; /* fixed width for labels */ + font-weight: 600; +} +.modal-info-grid p span { + flex: 1; +} diff --git a/frontend/src/css/reservation-v2-1.css b/frontend/src/css/reservation-v2-1.css index e8ccbeeb..93fda84c 100644 --- a/frontend/src/css/reservation-v2-1.css +++ b/frontend/src/css/reservation-v2-1.css @@ -1,43 +1,43 @@ /* General Container */ .reservation-v21-container { - padding: 40px; + width: 100%; max-width: 900px; - margin: 40px auto; - background-color: #ffffff; - border-radius: 16px; - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.07); - font-family: 'Toss Product Sans', sans-serif; - color: #333D4B; + margin: 2rem auto; + padding: 2rem; + font-family: 'Pretendard', sans-serif; + background-color: #fff; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); } .page-title { - font-size: 28px; - font-weight: 700; - margin-bottom: 40px; - color: #191F28; text-align: center; + font-size: 2rem; + font-weight: 700; + margin-bottom: 2.5rem; + color: #212529; } -/* Step Sections */ +/* Step Section */ .step-section { - margin-bottom: 40px; - padding: 24px; - border: 1px solid #E5E8EB; - border-radius: 12px; - transition: all 0.3s ease; + margin-bottom: 3rem; + padding: 1.5rem; + border: 1px solid #f1f3f5; + border-radius: 8px; + background-color: #f8f9fa; } .step-section.disabled { opacity: 0.5; pointer-events: none; - background-color: #F9FAFB; } .step-section h3 { - font-size: 20px; + font-size: 1.5rem; font-weight: 600; - margin-bottom: 20px; - color: #191F28; + margin-top: 0; + margin-bottom: 1.5rem; + color: #343a40; } /* Date Carousel */ @@ -45,274 +45,241 @@ display: flex; align-items: center; justify-content: space-between; - gap: 10px; - margin: 20px 0; + margin-bottom: 1rem; +} + +.carousel-arrow { + background: none; + border: none; + font-size: 2rem; + color: #868e96; + cursor: pointer; + padding: 0 1rem; } .date-options-container { display: flex; - gap: 8px; - overflow-x: hidden; - flex-grow: 1; - justify-content: space-between; - margin: 0px 15px; + gap: 10px; + overflow-x: auto; + -ms-overflow-style: none; + scrollbar-width: none; } -.carousel-arrow, .today-button { - background-color: #F2F4F6; - border: 1px solid #E5E8EB; - border-radius: 50%; - width: 36px; - height: 36px; - font-size: 20px; - font-weight: bold; - color: #4E5968; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - transition: background-color 0.2s; -} - -.today-button { - border-radius: 8px; - font-size: 14px; - font-weight: 600; - width: auto; - padding: 0 15px; -} - -.carousel-arrow:hover, .today-button:hover { - background-color: #E5E8EB; +.date-options-container::-webkit-scrollbar { + display: none; } .date-option { + text-align: center; cursor: pointer; - padding: 8px; - border-radius: 8px; + padding: 10px; + border-radius: 50%; + width: 60px; + height: 60px; display: flex; flex-direction: column; - align-items: center; justify-content: center; - border: 1px solid transparent; - transition: all 0.3s ease; - width: 60px; - flex-shrink: 0; -} - -.date-option:hover { - background-color: #f0f0f0; -} - -.date-option.active { - border: 1px solid #007bff; - background-color: #e7f3ff; + align-items: center; + transition: background-color 0.3s, color 0.3s; } .date-option .day-of-week { - font-size: 12px; - color: #888; -} - -.date-option.active .day-of-week { - color: #007bff; + font-size: 0.8rem; + margin-bottom: 4px; } .date-option .day-circle { - font-size: 16px; - font-weight: bold; - width: 32px; - height: 32px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - margin-top: 4px; - background-color: #f0f0f0; - color: #333; + font-weight: 600; } -.date-option.active .day-circle { - background-color: #007bff; +.date-option.active { + background-color: #0064FF; color: white; } +.date-option:not(.active):hover { + background-color: #f1f3f5; +} + .date-option.disabled { - opacity: 0.5; + color: #ced4da; cursor: not-allowed; - pointer-events: none; } -.date-option.disabled .day-circle { - background-color: #E5E8EB; - color: #B0B8C1; -} - - -/* Theme List */ -.theme-list { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 16px; -} - -.theme-card { +.today-button { + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 20px; + padding: 0.5rem 1rem; cursor: pointer; - border-radius: 12px; - overflow: hidden; - border: 2px solid #E5E8EB; - transition: all 0.2s ease-in-out; + margin-left: 1rem; + font-weight: 500; +} + +/* --- Region & Store Selectors --- */ +.region-store-selectors { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.region-store-selectors select { + flex: 1; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 8px; background-color: #fff; + font-size: 1rem; + cursor: pointer; + transition: border-color 0.2s; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23868e96%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'); + background-repeat: no-repeat; + background-position: right .7em top 50%; + background-size: .65em auto; +} + +.region-store-selectors select:disabled { + background-color: #f8f9fa; + cursor: not-allowed; + color: #adb5bd; +} + +.region-store-selectors select:focus { + outline: none; + border-color: #0064FF; +} + +/* --- Schedule List --- */ +.schedule-list { display: flex; flex-direction: column; + gap: 1.5rem; } -.theme-card:hover { - transform: translateY(-4px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +.theme-schedule-group { + background-color: #fff; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 1.5rem; } -.theme-card.active { - border-color: #3182F6; - box-shadow: 0 0 0 3px rgba(49, 130, 246, 0.2); -} - -.theme-thumbnail { - width: 100%; - height: 120px; - object-fit: cover; -} - -.theme-info { - padding: 16px; +.theme-header { display: flex; - flex-direction: column; - flex-grow: 1; + justify-content: space-between; + align-items: center; + padding-bottom: 1rem; + margin-bottom: 1rem; + border-bottom: 1px solid #f1f3f5; } -.theme-info h4 { - font-size: 16px; - font-weight: 600; - margin-bottom: 8px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.theme-info p { - font-size: 14px; - color: #6B7684; +.theme-header h4 { margin: 0; -} - -.theme-meta { - font-size: 14px; - color: #4E5968; - margin-bottom: 12px; - flex-grow: 1; -} - -.theme-meta p { - margin: 2px 0; -} -.theme-meta strong { - color: #333D4B; + font-size: 1.25rem; + font-weight: 600; + color: #343a40; } .theme-detail-button { - width: 100%; - padding: 8px; - font-size: 14px; - font-weight: 600; - border: none; - background-color: #F2F4F6; - color: #4E5968; - border-radius: 8px; + padding: 0.5rem 1rem; + font-size: 0.9rem; + background-color: transparent; + color: #0064FF; + border: 1px solid #0064FF; + border-radius: 6px; cursor: pointer; - transition: background-color 0.2s; + font-weight: 600; + transition: background-color 0.2s, color 0.2s; } .theme-detail-button:hover { - background-color: #E5E8EB; + background-color: #0064FF; + color: #fff; } /* Time Slots */ .time-slots { display: grid; - grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); - gap: 12px; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 0.75rem; } .time-slot { - cursor: pointer; - padding: 16px; - border-radius: 8px; + padding: 0.75rem; + border: 1px solid #dee2e6; + border-radius: 6px; text-align: center; - background-color: #F2F4F6; - font-weight: 600; - transition: all 0.2s ease-in-out; - position: relative; + cursor: pointer; + transition: all 0.2s; + background-color: #fff; } -.time-slot:hover { - background-color: #E5E8EB; +.time-slot:hover:not(.disabled) { + border-color: #0064FF; + color: #0064FF; } .time-slot.active { - background-color: #3182F6; - color: #ffffff; + background-color: #0064FF; + color: white; + border-color: #0064FF; + font-weight: 600; } .time-slot.disabled { - background-color: #F9FAFB; - color: #B0B8C1; + background-color: #f8f9fa; + color: #adb5bd; cursor: not-allowed; text-decoration: line-through; } .time-availability { - font-size: 12px; display: block; + font-size: 0.8rem; margin-top: 4px; - font-weight: 500; } .no-times { + color: #868e96; + padding: 2rem; text-align: center; - padding: 20px; - color: #8A94A2; + background-color: #fff; + border-radius: 8px; } -/* Next Step Button */ +/* --- Next Step Button --- */ .next-step-button-container { - display: flex; - justify-content: flex-end; - margin-top: 30px; + margin-top: 2rem; + text-align: center; } .next-step-button { - padding: 14px 28px; - font-size: 18px; + width: 100%; + max-width: 400px; + padding: 1rem; + font-size: 1.2rem; font-weight: 700; + color: #fff; + background-color: #0064FF; border: none; - background-color: #3182F6; - color: #ffffff; - border-radius: 12px; + border-radius: 8px; cursor: pointer; transition: background-color 0.2s; } +.next-step-button:hover:not(:disabled) { + background-color: #0053d1; +} + .next-step-button:disabled { - background-color: #B0B8C1; + background-color: #a0a0a0; cursor: not-allowed; } -.next-step-button:hover:not(:disabled) { - background-color: #1B64DA; -} -/* Modal Styles */ +/* --- Modal Styles --- */ .modal-overlay { position: fixed; top: 0; @@ -328,170 +295,158 @@ .modal-content { background-color: #ffffff !important; - padding: 32px !important; - border-radius: 16px !important; + padding: 2rem !important; + border-radius: 12px !important; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3) !important; width: 90% !important; max-width: 500px !important; position: relative !important; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1) !important; + max-height: 90vh !important; + overflow-y: auto !important; } .modal-close-button { position: absolute; - top: 16px; - right: 16px; + top: 1rem; + right: 1rem; background: none; border: none; - font-size: 24px; + font-size: 1.5rem; + color: #868e96; cursor: pointer; - color: #8A94A2; } .modal-theme-thumbnail { width: 100%; height: 200px; object-fit: cover; - border-radius: 12px; - margin-bottom: 24px; + border-radius: 8px; + margin-bottom: 1.5rem; } .modal-content h2 { - font-size: 24px; - font-weight: 700; - margin-bottom: 24px; - color: #191F28; + margin-top: 0; + margin-bottom: 2rem; + text-align: center; } .modal-section { - margin-bottom: 20px; + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid #f1f3f5; +} + +.modal-section:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; } .modal-section h3 { - font-size: 18px; - font-weight: 600; - margin-bottom: 12px; - border-bottom: 1px solid #E5E8EB; - padding-bottom: 8px; + margin-top: 0; + margin-bottom: 1rem; + font-size: 1.1rem; + color: #495057; } .modal-section p { - font-size: 16px; + margin: 0.5rem 0; + color: #495057; line-height: 1.6; - margin-bottom: 8px; - color: #4E5968; -} - -.modal-section p strong { - color: #333D4B; - margin-right: 8px; } .modal-actions { display: flex; justify-content: flex-end; - gap: 12px; - margin-top: 30px; + gap: 1rem; + margin-top: 2rem; } -.modal-actions button { - padding: 12px 24px; - font-size: 16px; - font-weight: 600; +.modal-actions .cancel-button, +.modal-actions .confirm-button { + padding: 0.75rem 1.5rem; border-radius: 8px; - cursor: pointer; border: none; - transition: background-color 0.2s; + font-size: 1rem; + font-weight: 600; + cursor: pointer; } .modal-actions .cancel-button { - background-color: #E5E8EB; - color: #4E5968; -} -.modal-actions .cancel-button:hover { - background-color: #D1D6DB; + background-color: #f1f3f5; + color: #495057; } .modal-actions .confirm-button { - background-color: #3182F6; - color: #ffffff; -} -.modal-actions .confirm-button:hover { - background-color: #1B64DA; + background-color: #0064FF; + color: #fff; } -/* Styles for ReservationFormPage */ +/* --- Form Styles for ReservationFormPage --- */ .form-group { - margin-bottom: 20px; + margin-bottom: 1rem; } .form-group label { display: block; - font-weight: bold; - margin-bottom: 8px; - color: #333; + margin-bottom: 0.5rem; + font-weight: 600; + color: #495057; } -.form-group input[type="text"], -.form-group input[type="tel"], -.form-group input[type="number"], -.form-group textarea { +.form-input { width: 100%; - padding: 12px; - border: 1px solid #ccc; + padding: 0.75rem; + border: 1px solid #ddd; border-radius: 8px; - font-size: 16px; - box-sizing: border-box; - transition: border-color 0.2s, box-shadow 0.2s; + font-size: 1rem; } -.form-group input:focus, .form-group textarea:focus { - outline: none; - border-color: #3182F6; - box-shadow: 0 0 0 3px rgba(49, 130, 246, 0.2); -} - -.form-group textarea { - resize: vertical; - min-height: 100px; -} - -.participant-control { - display: flex; - align-items: center; -} - -.participant-control input { +/* Success Page */ +.success-icon { + font-size: 4rem; + color: #0064FF; text-align: center; - border-left: none; - border-right: none; - width: 60px; - border-radius: 0; + margin-bottom: 1.5rem; } -.participant-control button { - width: 44px; - height: 44px; - border: 1px solid #ccc; - background-color: #f0f0f0; - font-size: 20px; - cursor: pointer; +.success-page-actions { + display: flex; + justify-content: center; + gap: 1rem; + margin-top: 2.5rem; +} + +.success-page-actions .action-button { + padding: 0.8rem 1.6rem; + border-radius: 8px; + text-decoration: none; + font-size: 1rem; + font-weight: 600; transition: background-color 0.2s; } -.participant-control button:hover:not(:disabled) { - background-color: #e0e0e0; +.success-page-actions .action-button.secondary { + background-color: #f1f3f5; + color: #495057; } -.participant-control button:disabled { - background-color: #e9ecef; - cursor: not-allowed; - color: #aaa; +.success-page-actions .action-button:not(.secondary) { + background-color: #0064FF; + color: #fff; } -.participant-control button:first-of-type { - border-radius: 8px 0 0 8px; +/* Added for modal info alignment */ +.modal-info-grid p { + display: flex; + align-items: flex-start; + margin: 0.6rem 0; + line-height: 1.5; } - -.participant-control button:last-of-type { - border-radius: 0 8px 8px 0; +.modal-info-grid p strong { + flex: 0 0 130px; /* fixed width for labels */ + font-weight: 600; +} +.modal-info-grid p span { + flex: 1; } diff --git a/frontend/src/css/signup-page-v2.css b/frontend/src/css/signup-page-v2.css index 7918185b..8d95c48b 100644 --- a/frontend/src/css/signup-page-v2.css +++ b/frontend/src/css/signup-page-v2.css @@ -68,4 +68,13 @@ color: #E53E3E; font-size: 12px; margin-top: 4px; -} \ No newline at end of file +} + +.region-select-group { + display: flex; + gap: 10px; +} + +.region-select-group select { + flex: 1; +} diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index a5c22119..e16e9d9c 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -2,8 +2,8 @@ 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 {mapThemeResponse, type ThemeInfoResponse} from '@_api/theme/themeTypes'; +import {fetchThemesByIds} from '@_api/theme/themeAPI'; +import {DifficultyKoreanMap, mapThemeResponse, type ThemeInfoResponse} from '@_api/theme/themeTypes'; const HomePage: React.FC = () => { const [ranking, setRanking] = useState([]); @@ -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); @@ -71,11 +71,12 @@ const HomePage: React.FC = () => {

{selectedTheme.name}

{selectedTheme.description}

-
-

난이도: {selectedTheme.difficulty}

-

가격: {selectedTheme.price.toLocaleString()}원

-

예상 시간: {selectedTheme.expectedMinutesFrom} ~ {selectedTheme.expectedMinutesTo}분

-

이용 가능 인원: {selectedTheme.minParticipants} ~ {selectedTheme.maxParticipants}명

+
+

난이도:{DifficultyKoreanMap[selectedTheme.difficulty]}

+

이용 가능 인원:{selectedTheme.minParticipants} ~ {selectedTheme.maxParticipants}명

+

1인당 요금:{selectedTheme.price.toLocaleString()}원

+

예상 시간:{selectedTheme.expectedMinutesFrom} ~ {selectedTheme.expectedMinutesTo}분

+

이용 가능 시간:{selectedTheme.availableMinutes}분

diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index db4704d3..61474db7 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -15,11 +15,11 @@ const LoginPage: React.FC = () => { const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); try { - const principalType = from.startsWith('/admin') ? 'ADMIN' : 'USER'; - await login({ account: email, password: password, principalType: principalType }); + await login({ account: email, password: password }); alert('로그인에 성공했어요!'); - navigate(from, { replace: true }); + const redirectTo = from.startsWith('/admin') ? '/' : from; + navigate(redirectTo, { replace: true }); } catch (error: any) { const message = error.response?.data?.message || '로그인에 실패했어요. 이메일과 비밀번호를 확인해주세요.'; alert(message); diff --git a/frontend/src/pages/MyReservationPage.tsx b/frontend/src/pages/MyReservationPage.tsx index d25d07f8..539f2b8b 100644 --- a/frontend/src/pages/MyReservationPage.tsx +++ b/frontend/src/pages/MyReservationPage.tsx @@ -1,17 +1,18 @@ -import {cancelPayment} from '@_api/payment/paymentAPI'; -import type {PaymentRetrieveResponse} from '@_api/payment/PaymentTypes'; -import {cancelReservation, fetchDetailById, fetchSummaryByMember} from '@_api/reservation/reservationAPI'; +import { cancelPayment } from '@_api/payment/paymentAPI'; +import type { PaymentRetrieveResponse } from '@_api/payment/PaymentTypes'; +import { cancelReservation, fetchDetailById, fetchAllOverviewByUser } from '@_api/reservation/reservationAPI'; import { - type ReservationDetail, - ReservationStatus, - type ReservationSummaryRetrieveResponse + ReservationStatus, + type ReservationDetail, + type ReservationOverviewResponse } from '@_api/reservation/reservationTypes'; -import React, {useEffect, useState} from 'react'; import '@_css/my-reservation-v2.css'; +import { formatDate, formatDisplayDateTime, formatTime } from '@_util/DateTimeFormatter'; +import React, { useEffect, useState } from 'react'; -const getReservationStatus = (reservation: ReservationSummaryRetrieveResponse): { className: string, text: string } => { +const getReservationStatus = (reservation: ReservationOverviewResponse): { className: string, text: string } => { const now = new Date(); - const reservationDateTime = new Date(`${reservation.date}T${reservation.startAt}`); + const reservationDateTime = new Date(`${reservation.date}T${reservation.startFrom}`); switch (reservation.status) { case ReservationStatus.CANCELED: @@ -22,81 +23,12 @@ const getReservationStatus = (reservation: ReservationSummaryRetrieveResponse): } return { className: 'status-confirmed', text: '예약확정' }; case ReservationStatus.PENDING: - return { className: 'status-pending', text: '입금대기' }; + return { className: 'status-pending', text: '입금대기' }; default: return { className: `status-${reservation.status.toLowerCase()}`, text: reservation.status }; } }; -const formatDisplayDateTime = (dateTime: any): string => { - let date: Date; - - if (typeof dateTime === 'string') { - // ISO 문자열 형식 처리 (LocalDateTime, OffsetDateTime 모두 포함) - date = new Date(dateTime); - } else if (typeof dateTime === 'number') { - // Unix 타임스탬프(초) 형식 처리 - date = new Date(dateTime * 1000); - } else if (Array.isArray(dateTime) && dateTime.length >= 6) { - // 배열 형식 처리: [year, month, day, hour, minute, second, nanosecond?] - const year = dateTime[0]; - const month = dateTime[1] - 1; // JS Date의 월은 0부터 시작 - const day = dateTime[2]; - const hour = dateTime[3]; - const minute = dateTime[4]; - const second = dateTime[5]; - const millisecond = dateTime.length > 6 ? Math.floor(dateTime[6] / 1000000) : 0; - date = new Date(year, month, day, hour, minute, second, millisecond); - } else { - return '유효하지 않은 날짜 형식'; - } - - if (isNaN(date.getTime())) { - return '유효하지 않은 날짜'; - } - - const options: Intl.DateTimeFormatOptions = { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - hour12: true, - second: 'numeric' - }; - return new Intl.DateTimeFormat('ko-KR', options).format(date); -}; - -const formatCardDateTime = (dateStr: string, timeStr: string): string => { - const date = new Date(`${dateStr}T${timeStr}`); - const currentYear = new Date().getFullYear(); - const reservationYear = date.getFullYear(); - - const days = ['일', '월', '화', '수', '목', '금', '토']; - const dayOfWeek = days[date.getDay()]; - const month = date.getMonth() + 1; - const day = date.getDate(); - let hours = date.getHours(); - const minutes = date.getMinutes(); - const ampm = hours >= 12 ? '오후' : '오전'; - hours = hours % 12; - hours = hours ? hours : 12; - - let datePart = ''; - if (currentYear === reservationYear) { - datePart = `${month}월 ${day}일(${dayOfWeek})`; - } else { - datePart = `${reservationYear}년 ${month}월 ${day}일(${dayOfWeek})`; - } - - let timePart = `${ampm} ${hours}시`; - if (minutes !== 0) { - timePart += ` ${minutes}분`; - } - - return `${datePart} ${timePart}`; -}; - // --- Cancellation View Component --- const CancellationView: React.FC<{ reservation: ReservationDetail; @@ -117,10 +49,10 @@ const CancellationView: React.FC<{ return (

취소 정보 확인

-
-

테마: {reservation.themeName}

-

신청 일시: {formatDisplayDateTime(reservation.applicationDateTime)}

- {reservation.payment &&

결제 금액: {reservation.payment.totalAmount.toLocaleString()}원

} +
+

테마:{reservation.overview.themeName}

+

신청 일시:{formatDisplayDateTime(reservation.applicationDateTime)}

+ {reservation.payment &&

결제 금액:{reservation.payment.totalAmount.toLocaleString()}원

}