import 'server-only'; import { getSessionAccessToken } from '@/lib/auth/dal'; import { BackendRequestError, backendFetch } from '@/lib/http/backend-fetch'; import { buildFileImageUrl } from '@/lib/data/repositories/file-repository'; import { joinProductBadges, parseProductBadges, type MerchantProduct, } from '@/lib/domain/merchant-product'; import type { MerchantProductValues } from '@/lib/domain/merchant-product-form'; /** * 가맹몰 메뉴 Repository — edupay-backend origin/develop 15d409a 기준. * * ``` * GET /api/v1/mngr/voc/product/list/{frcsNo} 상세가 있는 가맹점의 메뉴 전체(페이징 없음) * GET /api/v1/mngr/voc/product/{frcsProductSn} * POST /api/v1/mngr/voc/product @RequestBody · 가맹점 상세가 있어야 한다 * PUT /api/v1/mngr/voc/product/{frcsProductSn} @RequestBody * DELETE /api/v1/mngr/voc/product/{frcsProductSn} 논리 삭제 * ``` */ const PRODUCT_PATH = '/api/v1/mngr/voc/product'; function isRecord(value: unknown): value is Record { return value !== null && typeof value === 'object'; } function readString(source: Record, key: string): string | null { const value = source[key]; if (typeof value === 'string' && value !== '') return value; if (typeof value === 'number') return String(value); return null; } function readNumber(source: Record, key: string): number | null { const value = source[key]; if (typeof value === 'number' && Number.isFinite(value)) return value; if (typeof value === 'string' && value.trim() !== '') { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } return null; } function toProduct(raw: unknown): MerchantProduct[] { if (!isRecord(raw)) return []; const id = readString(raw, 'frcsProductSn'); const frcsNo = readString(raw, 'frcsNo'); if (!id || !frcsNo) return []; return [ { id, frcsNo, name: readString(raw, 'frcsProductSj') ?? '', price: readNumber(raw, 'frcsProductPrice'), description: readString(raw, 'frcsProductCn'), badges: parseProductBadges(readString(raw, 'frcsBadge')), imageFileId: readString(raw, 'atchFileId'), imageUrl: buildFileImageUrl(readString(raw, 'atchFileId')), sortOrder: readNumber(raw, 'sortOrdr'), isVisible: (readString(raw, 'useYn') ?? 'Y').toUpperCase() !== 'N', }, ]; } export async function fetchMerchantProducts(frcsNo: string): Promise { const accessToken = await getSessionAccessToken(); const result = await backendFetch( `${PRODUCT_PATH}/list/${encodeURIComponent(frcsNo)}`, { method: 'GET', accessToken: accessToken ?? undefined, cache: 'no-store', canHaveNullData: true, } ); if (!result.ok) { throw new BackendRequestError(result); } return Array.isArray(result.data) ? result.data.flatMap(toProduct) : []; } export async function fetchMerchantProduct( productSn: string ): Promise { const accessToken = await getSessionAccessToken(); const result = await backendFetch( `${PRODUCT_PATH}/${encodeURIComponent(productSn)}`, { method: 'GET', accessToken: accessToken ?? undefined, cache: 'no-store', canHaveNullData: true, } ); if (!result.ok) { // 없는 메뉴는 404 봉투로 온다 — 화면이 notFound()로 넘기게 null을 준다. if (result.code === 404) return null; throw new BackendRequestError(result); } return toProduct(result.data)[0] ?? null; } function toWritePayload(values: MerchantProductValues): Record { return { frcsNo: values.frcsNo, frcsProductSj: values.name, frcsProductPrice: values.price, frcsProductCn: values.description, frcsBadge: joinProductBadges(values.badges), atchFileId: values.imageFileId || null, sortOrdr: values.sortOrder, useYn: values.isVisible ? 'Y' : 'N', }; } async function sendWrite( path: string, method: 'POST' | 'PUT' | 'DELETE', body?: unknown ): Promise { const accessToken = await getSessionAccessToken(); const result = await backendFetch(path, { method, ...(body === undefined ? {} : { body }), accessToken: accessToken ?? undefined, cache: 'no-store', canHaveNullData: true, canHaveEmptyBody: true, }); if (!result.ok) { throw new BackendRequestError(result); } } export async function createMerchantProduct( values: MerchantProductValues ): Promise { await sendWrite(PRODUCT_PATH, 'POST', toWritePayload(values)); } export async function updateMerchantProduct( productSn: string, values: MerchantProductValues ): Promise { await sendWrite( `${PRODUCT_PATH}/${encodeURIComponent(productSn)}`, 'PUT', toWritePayload(values) ); } export async function deleteMerchantProduct(productSn: string): Promise { await sendWrite(`${PRODUCT_PATH}/${encodeURIComponent(productSn)}`, 'DELETE'); }