Skip to content
5 changes: 4 additions & 1 deletion src/app/api/auth/access-token/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ export async function POST() {
throw createMissingRefreshTokenError();
}

const serverApi = await createServerApi({accessToken: refreshToken});
const serverApi = await createServerApi({
accessToken: refreshToken,
authCookieName: 'refreshToken',
});
const {data} = await serverApi.post<AdminApiTypes.GetAccessTokenResponse>(
API_ENDPOINTS.auth.accessToken,
undefined,
Expand Down
61 changes: 61 additions & 0 deletions src/app/api/stores/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {NextRequest, NextResponse} from 'next/server';

import {API_ENDPOINTS, type AdminApiTypes} from '@/src/shared/api';
import {normalizeApiError} from '@/src/shared/api/api-error';
import {createServerApi} from '@/src/shared/api/server-client';
import {AUTH_COOKIE_NAMES} from '@/src/shared/auth';

const DEFAULT_PAGE = 1;
const DEFAULT_PAGE_SIZE = 10;

const createApiErrorResponse = (error: unknown) => {
const apiError = normalizeApiError(error);

return NextResponse.json(
{
code: apiError.code,
message: apiError.message,
},
{
status: apiError.status ?? 500,
}
);
};

const getPositiveIntegerParam = (
searchParams: URLSearchParams,
name: string,
fallback: number
) => {
const value = Number(searchParams.get(name));

return Number.isInteger(value) && value > 0 ? value : fallback;
};

export async function GET(request: NextRequest) {
const {searchParams} = new URL(request.url);
const keyword = searchParams.get('keyword')?.trim();
const page = getPositiveIntegerParam(searchParams, 'page', DEFAULT_PAGE);
const size = getPositiveIntegerParam(searchParams, 'size', DEFAULT_PAGE_SIZE);
const accessToken = request.cookies.get(AUTH_COOKIE_NAMES.accessToken)?.value;

try {
const serverApi = await createServerApi({accessToken});
const {data} =
await serverApi.get<AdminApiTypes.PageResponseGetStoreListResponse>(
API_ENDPOINTS.stores.root,
{
maxRedirects: 0,
params: {
...(keyword ? {keyword} : {}),
page,
size,
},
}
);

return NextResponse.json(data);
} catch (error) {
return createApiErrorResponse(error);
}
}
4 changes: 2 additions & 2 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {StoreManagementPage} from '@/src/features/store-management/StoreManagementPage';
import {redirect} from 'next/navigation';

export default function DashboardPage() {
return <StoreManagementPage />;
redirect('/dashboard/stores');
}
5 changes: 5 additions & 0 deletions src/app/dashboard/stores/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {StoreManagementPage} from '@/src/features/store-management/StoreManagementPage';

export default function StoresPage() {
return <StoreManagementPage />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -153,16 +153,13 @@ function HistoryTableRow({history}: {history: HistoryRecord}) {
}

function HistoryActionTag({action}: {action: HistoryAction}) {
const isCreate = action === 'create';

return (
<span
className={cn(
'text-body4 inline-flex h-[22px] min-w-10 items-center justify-center rounded-lg px-2',
isCreate
className={`text-body4 inline-flex h-[22px] min-w-10 items-center justify-center rounded-lg px-2 ${
action === 'create'
? 'bg-riu-monochrome-800 text-white'
: 'bg-riu-monochrome-20 text-riu-monochrome-800'
)}>
}`}>
{actionLabels[action]}
</span>
);
Expand Down
21 changes: 18 additions & 3 deletions src/features/store-management/StoreManagementPage.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
'use client';

import {PageTitle} from '@/src/shared/components/layout/PageTitle';

import {StoreAddDialogTrigger} from './components/StoreAddDialogTrigger';
import {
StoreManagementClientProvider,
useStoreManagementSummary,
} from './components/StoreManagementClientProvider';
import {StoreManagementTable} from './components/StoreManagementTable';
import {stores} from './model/mockStores';

function StoreManagementPage() {
return (
<StoreManagementClientProvider>
<StoreManagementPageContent />
</StoreManagementClientProvider>
);
}

function StoreManagementPageContent() {
const {totalElements} = useStoreManagementSummary();

return (
<section aria-labelledby='store-management-title' className='min-w-0'>
<PageTitle
title={<span id='store-management-title'>매장 관리</span>}
subtitle={`총 ${stores.length}개의 매장`}
subtitle={`총 ${totalElements}개의 매장`}
action={<StoreAddDialogTrigger />}
/>

<StoreManagementTable stores={stores} />
<StoreManagementTable />
</section>
);
}
Expand Down
81 changes: 81 additions & 0 deletions src/features/store-management/api/store-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {
API_ENDPOINTS,
buildApiPath,
getBrowserApi,
type AdminApiTypes,
} from '@/src/shared/api';

import type {Store, StoreStatus} from '../model/store';

type GetStoreListParams = {
keyword?: string;
page: number;
size: number;
};

type StoreListResult = {
stores: Store[];
page: number;
size: number;
totalElements: number;
totalPages: number;
hasNextPage: boolean;
};

type ApiStoreStatus = NonNullable<AdminApiTypes.GetStoreListResponse['status']>;

const storeStatusMap = {
OPEN_SOON: 'upcoming',
NEW_OPEN: 'new',
NORMAL: 'operating',
RENEWAL: 'renovation',
CLOSING_SOON: 'closing',
CLOSED: 'closed',
} satisfies Record<ApiStoreStatus, StoreStatus>;

const mapApiStoreStatus = (
status: AdminApiTypes.GetStoreListResponse['status']
): StoreStatus => {
return status ? storeStatusMap[status] : 'operating';
};

const mapApiStore = (
store: AdminApiTypes.GetStoreListResponse,
index: number
): Store => {
return {
id: store.id ?? index + 1,
name: store.name ?? '',
address: store.address ?? '',
station: store.station ?? '',
status: mapApiStoreStatus(store.status),
phone: store.contact ?? '',
website: store.websiteUrl ?? '',
};
};

export const getStoreList = async ({
keyword,
page,
size,
}: GetStoreListParams): Promise<StoreListResult> => {
const {data} =
await getBrowserApi().get<AdminApiTypes.PageResponseGetStoreListResponse>(
buildApiPath(API_ENDPOINTS.stores.root, {
keyword: keyword?.trim() || undefined,
page,
size,
})
);

return {
stores: data.contents?.map(mapApiStore) ?? [],
page: data.page ?? page,
size: data.size ?? size,
totalElements: data.totalElements ?? 0,
totalPages: data.totalPages ?? 1,
hasNextPage: data.hasNextPage ?? false,
};
};

export type {GetStoreListParams, StoreListResult};
24 changes: 24 additions & 0 deletions src/features/store-management/api/store-queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {useQuery} from '@tanstack/react-query';

import {
getStoreList,
type GetStoreListParams,
type StoreListResult,
} from './store-api';

const storeQueryKeys = {
all: ['stores'] as const,
lists: () => [...storeQueryKeys.all, 'list'] as const,
list: (params: GetStoreListParams) =>
[...storeQueryKeys.lists(), params] as const,
};

const useStoreListQuery = (params: GetStoreListParams) => {
return useQuery<StoreListResult>({
queryKey: storeQueryKeys.list(params),
queryFn: () => getStoreList(params),
placeholderData: (previousData) => previousData,
});
};

export {storeQueryKeys, useStoreListQuery};
Loading
Loading