diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index 63143b13..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,7 +49,9 @@ async function request( }, }; - const accessToken = localStorage.getItem('accessToken'); + const accessTokenKey = type === PrincipalType.ADMIN ? 'adminAccessToken' : 'accessToken'; + const accessToken = localStorage.getItem(accessTokenKey); + if (accessToken) { if (!config.headers) { config.headers = {}; @@ -70,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 daebb77a..141b3e45 100644 --- a/frontend/src/api/auth/authAPI.ts +++ b/frontend/src/api/auth/authAPI.ts @@ -12,20 +12,22 @@ export const userLogin = async ( return await apiClient.post( '/auth/login', { ...data, principalType: PrincipalType.USER }, - false, ); }; export const adminLogin = async ( data: Omit, ): Promise => { - return await apiClient.post( + return await apiClient.adminPost( '/auth/login', { ...data, principalType: PrincipalType.ADMIN }, - false, ); }; export const logout = async (): Promise => { - await apiClient.post('/auth/logout', {}, true); + await apiClient.post('/auth/logout', {}); }; + +export const adminLogout = async (): Promise => { + await apiClient.adminPost('/auth/logout', {}); +} diff --git a/frontend/src/api/reservation/reservationTypes.ts b/frontend/src/api/reservation/reservationTypes.ts index 5c19d21a..0de72737 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', diff --git a/frontend/src/api/schedule/scheduleAPI.ts b/frontend/src/api/schedule/scheduleAPI.ts index c4ee3cfc..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 fetchStoreAvailableThemesByDate = async (storeId: string, date: string): Promise => { - return await apiClient.get(`/stores/${storeId}/themes?date=${date}`); -}; +// admin +export const fetchAdminSchedules = async (storeId: string, date?: string, themeId?: string): Promise => { + const queryParams: string[] = []; -export const fetchStoreSchedulesByDateAndTheme = async (storeId: string, date: string, themeId: string): Promise => { - return await apiClient.get(`/stores/${storeId}/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 fetchScheduleDetailById = async (id: string): Promise => { - return await apiClient.get(`/admin/schedules/${id}`); -}; + return await apiClient.adminGet(fullUrl); +} + +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.post(`/admin/stores/${storeId}/schedules`, request); + return await apiClient.adminPost(`/admin/stores/${storeId}/schedules`, request); }; export const updateSchedule = async (id: string, request: ScheduleUpdateRequest): Promise => { - await apiClient.patch(`/admin/schedules/${id}`, request); + return await apiClient.adminPatch(`/admin/schedules/${id}`, request); }; export const deleteSchedule = async (id: string): Promise => { - await apiClient.del(`/admin/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 c31c9fb9..0acf9f64 100644 --- a/frontend/src/api/schedule/scheduleTypes.ts +++ b/frontend/src/api/schedule/scheduleTypes.ts @@ -1,4 +1,4 @@ -import type { OperatorInfo } from '@_api/common/commonTypes'; +import type { Difficulty } from '@_api/theme/themeTypes'; export type ScheduleStatus = 'AVAILABLE' | 'HOLD' | 'RESERVED' | 'BLOCKED'; @@ -9,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 { @@ -40,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: OperatorInfo; - updatedAt: string; // or Date - updatedBy: OperatorInfo; +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 index f9333bb0..80c9135c 100644 --- a/frontend/src/api/store/storeAPI.ts +++ b/frontend/src/api/store/storeAPI.ts @@ -1,22 +1,48 @@ import apiClient from '@_api/apiClient'; -import { type SimpleStoreResponse, type StoreDetailResponse, type StoreRegisterRequest, type UpdateStoreRequest } from './storeTypes'; +import type { + SimpleStoreListResponse, + StoreCreateResponse, + StoreDetailResponse, + StoreInfoResponse, + StoreRegisterRequest, + UpdateStoreRequest +} from './storeTypes'; -export const getStores = async (): Promise => { - return await apiClient.get('/admin/stores'); +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 getStoreDetail = async (id: number): Promise => { - return await apiClient.get(`/admin/stores/${id}`); +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 (data: StoreRegisterRequest): Promise => { - return await apiClient.post('/admin/stores', data); +export const createStore = async (request: StoreRegisterRequest): Promise => { + return await apiClient.adminPost('/admin/stores', request); }; -export const updateStore = async (id: number, data: UpdateStoreRequest): Promise => { - return await apiClient.put(`/admin/stores/${id}`, data); +export const updateStore = async (id: string, request: UpdateStoreRequest): Promise => { + await apiClient.adminPatch(`/admin/stores/${id}`, request); }; -export const deleteStore = async (id: number): Promise => { - await apiClient.del(`/admin/stores/${id}`); +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 index b036d664..376549c4 100644 --- a/frontend/src/api/store/storeTypes.ts +++ b/frontend/src/api/store/storeTypes.ts @@ -1,11 +1,15 @@ -import { type AuditInfo } from '@_api/common/commonTypes'; -import type { RegionInfoResponse } from '@_api/region/regionTypes'; +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; @@ -25,8 +29,20 @@ export interface StoreRegisterRequest { } export interface UpdateStoreRequest { + name?: string; + address?: string; + contact?: string; + regionCode?: string; +} + +export interface StoreInfoResponse { + id: string; name: string; address: string; contact: string; - regionCode: 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 43fb1516..f0f6bdf9 100644 --- a/frontend/src/api/theme/themeAPI.ts +++ b/frontend/src/api/theme/themeAPI.ts @@ -1,7 +1,7 @@ import apiClient from '@_api/apiClient'; import type { - AdminThemeDetailRetrieveResponse, - AdminThemeSummaryRetrieveListResponse, + AdminThemeDetailResponse, + AdminThemeSummaryListResponse, SimpleActiveThemeListResponse, ThemeCreateRequest, ThemeCreateResponse, @@ -11,24 +11,28 @@ import type { 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 fetchActiveThemes = async (): Promise => { + return await apiClient.adminGet('/admin/themes/active'); }; export const fetchThemesByIds = async (request: ThemeIdListResponse): Promise => { @@ -38,7 +42,3 @@ export const fetchThemesByIds = async (request: ThemeIdListResponse): 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 0cc043ed..2b56d825 100644 --- a/frontend/src/api/theme/themeTypes.ts +++ b/frontend/src/api/theme/themeTypes.ts @@ -1,22 +1,9 @@ -import type { OperatorInfo } from '@_api/common/commonTypes'; +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; + theme: ThemeInfoResponse; isActive: boolean; - createDate: string; // Assuming ISO string format - updatedDate: string; // Assuming ISO string format - createdBy: OperatorInfo; - updatedBy: OperatorInfo; + audit: AuditInfo } export interface ThemeCreateRequest { @@ -45,14 +32,13 @@ export interface ThemeUpdateRequest { price?: number; minParticipants?: number; maxParticipants?: number; - availableMinutes?: number; expectedMinutesFrom?: number; expectedMinutesTo?: number; isActive?: boolean; } -export interface AdminThemeSummaryRetrieveResponse { +export interface AdminThemeSummaryResponse { id: string; name: string; difficulty: Difficulty; @@ -60,27 +46,8 @@ export interface AdminThemeSummaryRetrieveResponse { 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; - isActive: boolean; - createdAt: string; // LocalDateTime in Kotlin, map to string (ISO format) - createdBy: OperatorInfo; - updatedAt: string; // LocalDateTime in Kotlin, map to string (ISO format) - updatedBy: OperatorInfo; +export interface AdminThemeSummaryListResponse { + themes: AdminThemeSummaryResponse[]; } export interface ThemeInfoResponse { @@ -135,4 +102,4 @@ export interface SimpleActiveThemeResponse { 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 3857aeec..ba6c7cd1 100644 --- a/frontend/src/context/AdminAuthContext.tsx +++ b/frontend/src/context/AdminAuthContext.tsx @@ -1,4 +1,4 @@ -import { adminLogin as apiLogin, logout as apiLogout } from '@_api/auth/authAPI'; +import { adminLogin as apiLogin, adminLogout as apiLogout } from '@_api/auth/authAPI'; import { type AdminLoginSuccessResponse, type AdminType, @@ -27,7 +27,7 @@ export const AdminAuthProvider: React.FC<{ children: ReactNode }> = ({ children useEffect(() => { try { - const token = localStorage.getItem('accessToken'); + const token = localStorage.getItem('adminAccessToken'); 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('accessToken', response.accessToken); + localStorage.setItem('adminAccessToken', 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('accessToken'); + localStorage.removeItem('adminAccessToken'); 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 c4cb2775..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 { @@ -239,13 +253,13 @@ th { } .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; + 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 { @@ -282,35 +296,25 @@ th { 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 + 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 index 7dc8924a..f8d5c0a4 100644 --- a/frontend/src/css/admin-store-page.css +++ b/frontend/src/css/admin-store-page.css @@ -16,6 +16,19 @@ 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; 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..7f3e5b93 100644 --- a/frontend/src/css/my-reservation-v2.css +++ b/frontend/src/css/my-reservation-v2.css @@ -177,16 +177,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 +240,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 +339,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/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 3d4461d5..e16e9d9c 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -3,7 +3,7 @@ import '@_css/home-page-v2.css'; import React, {useEffect, useState} from 'react'; import {useNavigate} from 'react-router-dom'; import {fetchThemesByIds} from '@_api/theme/themeAPI'; -import {mapThemeResponse, type ThemeInfoResponse} from '@_api/theme/themeTypes'; +import {DifficultyKoreanMap, mapThemeResponse, type ThemeInfoResponse} from '@_api/theme/themeTypes'; const HomePage: React.FC = () => { const [ranking, setRanking] = useState([]); @@ -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/MyReservationPage.tsx b/frontend/src/pages/MyReservationPage.tsx index 24565bfc..84000f53 100644 --- a/frontend/src/pages/MyReservationPage.tsx +++ b/frontend/src/pages/MyReservationPage.tsx @@ -117,10 +117,10 @@ const CancellationView: React.FC<{ return (

취소 정보 확인

-
-

테마: {reservation.themeName}

-

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

- {reservation.payment &&

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

} +
+

테마:{reservation.themeName}

+

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

+ {reservation.payment &&

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

}