File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
import type { CommonCode } from '@/lib/domain/common-code';
/**
* 가맹몰 상세 등록·수정 입력 규칙 — 순수 검증만 담는다.
*
* 기획 BO-MAL-002 ①의 필수 항목은 업체명·카테고리·업체 연락처·우편번호·노출 상태·바우처 사용·
* 대표이미지다. 업체 항목(업체명·연락처·우편번호·주소)은 이 화면에서 직접 입력한다(사용자 확정) —
* 가맹점이 없으면 새로 만들고, 있으면 함께 고친다.
*/
export const DESCRIPTION_MAX_LENGTH = 200;
export const CLOSED_DAYS_MAX_LENGTH = 100;
export const SORT_ORDER_MAX = 9999;
export const MERCHANT_NAME_MAX_LENGTH = 100;
export const ADDRESS_MAX_LENGTH = 200;
const TIME_PATTERN = /^([01]\d|2[0-3]):[0-5]\d$/;
const POSTAL_CODE_PATTERN = /^\d{5}$/;
const PHONE_PATTERN = /^[\d-]+$/;
/** 상세 화면이 함께 다루는 업체(가맹점) 항목 — 백엔드 `MngrVocBaseVo`의 일부. */
export type MerchantDetailMerchantValues = {
name: string;
phoneNumber: string;
postalCode: string;
address: string;
addressDetail: string;
};
export type MerchantDetailValues = MerchantDetailMerchantValues & {
/** 비어 있으면 가맹점을 새로 만든다. */
frcsNo: string;
categoryCode: string;
description: string;
openTime: string;
closeTime: string;
closedDays: string;
isVisible: boolean;
isVoucherUsable: boolean;
/** 빈 값이면 null — 기획 ⑦ 미입력 시 등록일 내림차순. */
sortOrder: number | null;
thumbnailFileId: string;
imageFileId: string;
};
export type MerchantDetailFormErrors = Partial<
Record<
| 'name'
| 'phoneNumber'
| 'postalCode'
| 'address'
| 'categoryCode'
| 'description'
| 'openTime'
| 'closeTime'
| 'closedDays'
| 'sortOrder'
| 'thumbnail'
| 'images',
string
>
>;
export type MerchantDetailFormState =
| { status: 'idle' }
| { status: 'error'; message?: string; errors?: MerchantDetailFormErrors }
/** `created`가 참이면 이번 저장으로 상세 행이 처음 생겼다 — 화면이 그 가맹점 주소로 옮겨 간다. */
| { status: 'success'; frcsNo: string; created: boolean };
export const INITIAL_MERCHANT_DETAIL_FORM_STATE: MerchantDetailFormState = {
status: 'idle',
};
type ValidationResult =
| { ok: true; values: MerchantDetailValues }
| { ok: false; errors: MerchantDetailFormErrors };
export function validateMerchantDetail(
values: MerchantDetailValues,
categories: readonly CommonCode[]
): ValidationResult {
const errors: MerchantDetailFormErrors = {};
const name = values.name.trim();
if (!name) {
errors.name = '업체명을 입력해 주세요.';
} else if (name.length > MERCHANT_NAME_MAX_LENGTH) {
errors.name = `업체명은 ${MERCHANT_NAME_MAX_LENGTH}자 이내로 입력해 주세요.`;
}
const phoneNumber = values.phoneNumber.trim();
if (!phoneNumber) {
errors.phoneNumber = '업체 연락처를 입력해 주세요.';
} else if (!PHONE_PATTERN.test(phoneNumber)) {
errors.phoneNumber = '연락처는 숫자와 -만 입력할 수 있습니다.';
}
const postalCode = values.postalCode.trim();
if (!postalCode) {
errors.postalCode = '우편번호를 입력해 주세요.';
} else if (!POSTAL_CODE_PATTERN.test(postalCode)) {
errors.postalCode = '우편번호는 숫자 5자리로 입력해 주세요.';
}
const address = values.address.trim();
const addressDetail = values.addressDetail.trim();
if (address.length > ADDRESS_MAX_LENGTH || addressDetail.length > ADDRESS_MAX_LENGTH) {
errors.address = `주소는 ${ADDRESS_MAX_LENGTH}자 이내로 입력해 주세요.`;
}
if (!categories.some((code) => code.code === values.categoryCode)) {
errors.categoryCode = '카테고리를 선택해 주세요.';
}
const description = values.description.trim();
if (description.length > DESCRIPTION_MAX_LENGTH) {
errors.description = `한줄 소개는 ${DESCRIPTION_MAX_LENGTH}자 이내로 입력해 주세요.`;
}
const openTime = values.openTime.trim();
const closeTime = values.closeTime.trim();
if (openTime && !TIME_PATTERN.test(openTime)) {
errors.openTime = '영업 시작 시간 형식이 올바르지 않습니다.';
}
if (closeTime && !TIME_PATTERN.test(closeTime)) {
errors.closeTime = '영업 종료 시간 형식이 올바르지 않습니다.';
}
const closedDays = values.closedDays.trim();
if (closedDays.length > CLOSED_DAYS_MAX_LENGTH) {
errors.closedDays = `휴무일은 ${CLOSED_DAYS_MAX_LENGTH}자 이내로 입력해 주세요.`;
}
if (
values.sortOrder !== null &&
(!Number.isInteger(values.sortOrder) ||
values.sortOrder < 0 ||
values.sortOrder > SORT_ORDER_MAX)
) {
errors.sortOrder = `노출 순서는 0 이상 ${SORT_ORDER_MAX} 이하의 숫자로 입력해 주세요.`;
}
if (!values.thumbnailFileId) {
errors.thumbnail = '대표이미지를 등록해 주세요.';
}
if (Object.keys(errors).length > 0) {
return { ok: false, errors };
}
return {
ok: true,
values: {
...values,
name,
phoneNumber,
postalCode,
address,
addressDetail,
description,
openTime,
closeTime,
closedDays,
},
};
}