File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
import 'server-only';
import {
createMockDecorationItem,
deleteMockDecorationItem,
listAllDecorationItems,
updateMockDecorationItem,
type CreateDecorationItemInput,
type UpdateDecorationItemInput,
} from '@/lib/data/mock/decoration-item-store';
import type { DecorationItem } from '@/lib/domain/decoration-item';
import type {
DecorationItemQuery,
DecorationItemSearchField,
} from '@/lib/domain/decoration-item-query';
/**
* 꾸미기 아이템 Repository — **백엔드에 이 도메인의 API가 전혀 없다**(조회조차 없음, 사용자
* 확정 사항 — edupay-backend 저장소에 대응 컨트롤러/서비스/매퍼가 없음을 확인했다). 그래서
* admin-member-repository.ts처럼 "백엔드 응답 + mock 오버레이" 구조가 아니라, 이 계층 전체가
* `lib/data/mock/decoration-item-store.ts` 하나로만 동작한다 — 조회·등록·수정·삭제 전부.
*
* **캐시 전략**: 실제 `fetch` 호출이 없어 `cache: 'no-store'`나 `next: { revalidate, tags }`
* 같은 HTTP 캐시 옵션을 붙일 대상이 없다(CLAUDE.md §2.6이 요구하는 "명시"는 여기서는 "해당
* 없음"을 명시하는 것으로 갈음한다). 대신 신선도는 두 가지로 보장한다:
* 1. 호출부인 `page.tsx`가 `verifySession()`으로 쿠키를 읽어 이미 이 라우트를 동적 렌더링으로
* 강제한다(요청마다 새로 실행).
* 2. 각 Server Action이 저장 직후 `revalidatePath(DECORATION_ITEMS_PATH)`로 라우터 캐시를
* 무효화한다.
* admin-member-repository.ts와 동일한 신선도 보장 방식이다.
*
* 백엔드에 API가 생기면 이 파일의 "쓰기 경로" 세 함수와 조회 함수의 본문만 실제 `backendFetch`
* 호출로 바꾸면 되고, 화면·Server Action은 그대로다.
*/
const SEARCH_VALUE_BY_FIELD: Record<
DecorationItemSearchField,
(item: DecorationItem) => string
> = {
name: (item) => item.name,
itemId: (item) => item.itemId,
};
function filterByKeyword(
items: DecorationItem[],
query: DecorationItemQuery
): DecorationItem[] {
const keyword = query.keyword.trim().toLowerCase();
if (!keyword) {
return items;
}
const readValue = SEARCH_VALUE_BY_FIELD[query.searchField];
return items.filter((item) => readValue(item).toLowerCase().includes(keyword));
}
/** 정렬순서 오름차순(시안 기본 정렬). 값이 같으면 아이템ID로 안정 정렬해 페이지를 넘나들 때
* 순서가 흔들리지 않게 한다. */
function sortBySortOrder(items: DecorationItem[]): DecorationItem[] {
return [...items].sort((a, b) =>
a.sortOrder !== b.sortOrder
? a.sortOrder - b.sortOrder
: a.itemId.localeCompare(b.itemId)
);
}
export type DecorationItemPage = {
items: DecorationItem[];
/** 검색어까지 적용한 결과 건수 — 요약 문구·페이지네이션 기준. */
totalCount: number;
/**
* 검색어와 무관하게 현재 유형(개별/셋트) 전체 등록 건수. 등록/수정 팝업의
* "정렬순서 (총 등록 N개)" 힌트와 신규 등록 기본 정렬순서(맨 끝에 추가) 계산에 쓴다 — 검색
* 결과가 아니라 그 유형에 실제로 존재하는 전체 개수여야 하므로 `totalCount`와 분리했다.
*/
typeTotalCount: number;
};
/** 검색·유형·페이징이 적용된 꾸미기 아이템 목록을 조회한다. */
export async function fetchDecorationItems(
query: DecorationItemQuery
): Promise<DecorationItemPage> {
const byType = listAllDecorationItems().filter(
(item) => item.itemType === query.itemType
);
const matched = sortBySortOrder(filterByKeyword(byType, query));
const offset = (query.page - 1) * query.pageSize;
return {
items: matched.slice(offset, offset + query.pageSize),
totalCount: matched.length,
typeTotalCount: byType.length,
};
}
/**
* 아이템ID 중복 확인. 시안에 admins의 [중복확인] 같은 별도 버튼은 없지만, 아이템ID가 유일
* 식별자이자 수정/삭제 대상 키라 등록 Server Action이 저장 직전에 반드시 확인한다
* (`_actions.ts` 참조) — 화면에는 이 확인이 보이지 않고 실패 시 필드 오류로만 나타난다.
*/
export async function isDecorationItemIdTaken(itemId: string): Promise<boolean> {
const normalized = itemId.trim().toLowerCase();
return listAllDecorationItems().some(
(item) => item.itemId.toLowerCase() === normalized
);
}
/*
* ─── 쓰기 경로 ────────────────────────────────────────────────────────────────
* 백엔드에 이 도메인의 API가 전혀 없어 세 함수 모두 mock 저장소에 위임한다
* (`lib/data/mock/decoration-item-store.ts`의 주석에 한계를 적어 두었다).
*/
export async function createDecorationItem(
input: CreateDecorationItemInput
): Promise<void> {
createMockDecorationItem(input);
}
export async function updateDecorationItem(
itemId: string,
input: UpdateDecorationItemInput
): Promise<void> {
updateMockDecorationItem(itemId, input);
}
export async function deleteDecorationItem(itemId: string): Promise<void> {
deleteMockDecorationItem(itemId);
}