File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
import 'server-only';
import type { Merchant } from '@/lib/domain/merchant';
import { MERCHANT_CATEGORY_OPTIONS } from '@/lib/domain/merchant';
import type { MerchantQuery } from '@/lib/domain/merchant-query';
import type { MerchantValues } from '@/lib/domain/merchant-form';
/**
* 가맹점 Repository — 기획 MCH_001/002_p/003_p.
*
* TODO(백엔드): 관리자용 가맹점 API가 아직 없다(`mngr` 도메인에 해당 컨트롤러가 없고,
* `common/qrauth`의 `CmmPaymentMerchantVo`는 결제 조회용이라 id·name·type·addr·telNo뿐이다).
* 아래 mock 저장소를 지우고 다른 Repository처럼 backendFetch + getSessionAccessToken으로
* 교체하면 호출부는 그대로 둘 수 있다.
*
* 이슈: 저장소가 모듈 메모리라 서버를 다시 띄우면 등록·수정·삭제가 초기값으로 되돌아간다.
*/
const TOTAL = 128;
const BRANDS = ['GS25', 'CU', '', '스타벅스', '', '교보문고'];
const CATEGORY_CODES = MERCHANT_CATEGORY_OPTIONS.map((option) => option.value);
const CITIES = [
'경기도 평택시 송탄1로',
'경상남도 양산시 배움터길',
'경기도 용인시 기흥구 죽전로',
'서울특별시 마포구 양화로',
'부산광역시 해운대구 센텀중앙로',
];
const BASE_DATE = Date.UTC(2026, 0, 5);
const DAY_MS = 24 * 60 * 60 * 1000;
function dateAt(offsetDays: number): string {
return new Date(BASE_DATE + offsetDays * DAY_MS).toISOString().slice(0, 10);
}
/** 인덱스로만 만든다 — 난수를 쓰면 새로고침마다 목록이 흔들린다. */
function buildMerchant(index: number): Merchant {
const serial = index + 1;
const brand = BRANDS[index % BRANDS.length];
const city = CITIES[index % CITIES.length];
return {
id: `MCH-${String(serial).padStart(6, '0')}`,
code: `MCH-${String(serial).padStart(6, '0')}`,
brandName: brand,
name: brand ? `${brand} ${city.split(' ').at(-1)}점` : `${city.split(' ').at(-1)}점`,
categoryCode: CATEGORY_CODES[index % CATEGORY_CODES.length],
businessNumber: `${100 + (index % 800)}-${10 + (index % 80)}-${String(10000 + index).slice(0, 5)}`,
ownerName: `대표${serial}`,
phoneNumber: `0${2 + (index % 6)}${String(10000000 + index * 7).slice(0, 8)}`,
address: `${city} ${(index % 90) + 1}`,
addressDetail: brand ? `1층 ${brand}` : '',
latitude: (37.0987 + (index % 50) * 0.01).toFixed(4),
longitude: (127.0736 + (index % 50) * 0.01).toFixed(4),
// 몇 곳은 미노출로 둬 필터가 실제로 걸리는지 보이게 한다.
isVisible: index % 7 !== 0,
memo: '',
createdAt: dateAt(index % 60),
updatedAt: dateAt((index % 60) + 15),
};
}
let MERCHANTS: Merchant[] = Array.from({ length: TOTAL }, (_, index) =>
buildMerchant(index)
);
const SEARCH_VALUE: Record<string, (merchant: Merchant) => string> = {
name: (merchant) => merchant.name,
businessNumber: (merchant) => merchant.businessNumber,
address: (merchant) => `${merchant.address} ${merchant.addressDetail}`,
};
function matchesQuery(merchant: Merchant, query: MerchantQuery): boolean {
if (query.category && merchant.categoryCode !== query.category) {
return false;
}
if (query.visibility === 'visible' && !merchant.isVisible) {
return false;
}
if (query.visibility === 'hidden' && merchant.isVisible) {
return false;
}
const keyword = query.keyword.trim().toLowerCase();
if (!keyword) {
return true;
}
const read = SEARCH_VALUE[query.searchField];
return read ? read(merchant).toLowerCase().includes(keyword) : true;
}
/** 기획 MCH_001 ④ — 최근등록순 정렬. */
function byNewestFirst(a: Merchant, b: Merchant): number {
return Number(b.code.slice(4)) - Number(a.code.slice(4));
}
export async function fetchMerchants(
query: MerchantQuery
): Promise<{ items: Merchant[]; totalCount: number }> {
const matched = MERCHANTS.filter((merchant) => matchesQuery(merchant, query));
matched.sort(byNewestFirst);
const start = (query.page - 1) * query.pageSize;
return {
items: matched.slice(start, start + query.pageSize),
totalCount: matched.length,
};
}
/** 엑셀 다운로드는 검색 조건과 무관하게 전체다(기획 MCH_001 ②). */
export async function fetchAllMerchants(): Promise<Merchant[]> {
return [...MERCHANTS].sort(byNewestFirst);
}
function nowDate(): string {
return new Date().toISOString().slice(0, 10);
}
/** 가맹점코드는 저장 시 자동 생성한다(기획 MCH_002_p ⑤). */
export async function createMerchant(values: MerchantValues): Promise<void> {
const nextSerial =
MERCHANTS.reduce(
(max, merchant) => Math.max(max, Number(merchant.code.slice(4)) || 0),
0
) + 1;
const code = `MCH-${String(nextSerial).padStart(6, '0')}`;
MERCHANTS = [
...MERCHANTS,
{
...values,
id: code,
code,
createdAt: nowDate(),
updatedAt: nowDate(),
},
];
}
export async function updateMerchant(values: MerchantValues): Promise<void> {
const index = MERCHANTS.findIndex((merchant) => merchant.id === values.id);
if (index < 0) {
throw new Error('가맹점을 찾을 수 없습니다.');
}
MERCHANTS[index] = {
...MERCHANTS[index],
...values,
// 코드·등록일은 바뀌지 않는다.
id: MERCHANTS[index].id,
code: MERCHANTS[index].code,
createdAt: MERCHANTS[index].createdAt,
updatedAt: nowDate(),
};
}
export async function deleteMerchant(id: string): Promise<void> {
const next = MERCHANTS.filter((merchant) => merchant.id !== id);
if (next.length === MERCHANTS.length) {
throw new Error('가맹점을 찾을 수 없습니다.');
}
MERCHANTS = next;
}