'use server';

import { revalidatePath } from 'next/cache';
import { redirect, unstable_rethrow } from 'next/navigation';
import { verifySession } from '@/lib/auth/dal';
import { BackendRequestError } from '@/lib/http/backend-fetch';
import { fetchCommonCodes } from '@/lib/data/repositories/common-code-repository';
import {
  buildFileImageUrl,
  fetchAttachmentFiles,
  uploadAttachment,
  uploadAttachments,
  type AttachmentFile,
} from '@/lib/data/repositories/file-repository';
import {
  createMerchantDetail,
  deleteMerchantDetail,
  fetchMerchantDetail,
  findMerchantByName,
  updateMerchantDetail,
} from '@/lib/data/repositories/merchant-detail-repository';
import { createMerchant, updateMerchant } from '@/lib/data/repositories/merchant-repository';
import {
  createMerchantProduct,
  deleteMerchantProduct,
  fetchMerchantProduct,
  updateMerchantProduct,
} from '@/lib/data/repositories/merchant-product-repository';
import { CODE_GROUP } from '@/lib/domain/common-code';
import {
  FILE_MODULE,
  MAX_ATTACHMENT_BYTES,
  allowedExtensions,
  isAllowedFileExtension,
} from '@/lib/domain/file-module';
import { MAX_EXTRA_IMAGES, MAX_IMAGE_BYTES } from '@/lib/domain/merchant-detail';
import {
  validateMerchantDetail,
  type MerchantDetailFormState,
  type MerchantDetailMerchantValues,
  type MerchantDetailValues,
} from '@/lib/domain/merchant-detail-form';
import { DEFAULT_MERCHANT_DIVISION_CODE } from '@/lib/domain/merchant';
import type { MerchantValues } from '@/lib/domain/merchant-form';
import {
  MERCHANT_DETAILS_PATH,
  merchantDetailPath,
} from '@/lib/domain/merchant-detail-query';
import type { MerchantProductBadge } from '@/lib/domain/merchant-product';
import {
  validateMerchantProduct,
  type MerchantProductFormState,
} from '@/lib/domain/merchant-product-form';

/**
 * 가맹몰 상세 Server Action — 상세(가맹점당 1건)와 메뉴(여러 건) 둘 다 여기서 다룬다.
 *
 * 노출 토글은 단건을 읽어 전 컬럼을 그대로 다시 보낸다 — 백엔드 `PUT`이 모든 컬럼을 덮어쓰기 때문이다.
 * 단건이 대표이미지·카테고리를 안 주는 응답이면(구 버전) 값이 지워지므로 토글을 거부한다.
 */

const SAVE_FAILED_MESSAGE = '저장하지 못했습니다. 잠시 후 다시 시도해 주세요.';
const DELETE_FAILED_MESSAGE = '삭제하지 못했습니다. 잠시 후 다시 시도해 주세요.';
const INVALID_REQUEST_MESSAGE = '요청이 올바르지 않습니다.';
const DETAIL_NOT_LOADED_MESSAGE =
  '백엔드가 상세 값을 돌려주지 않아 목록에서는 바꿀 수 없습니다. 수정 화면에서 바꿔 주세요.';
// 백엔드가 삭제된 상세도 "있는 것"으로 보는 결함(보고함) — 409는 대개 이 경우다.
const DETAIL_CONFLICT_MESSAGE =
  '이 가맹점에는 이미 상세가 있습니다. 삭제했던 상세라면 백엔드에서 정리해야 다시 등록할 수 있습니다.';
const MERCHANT_NOT_FOUND_AFTER_CREATE_MESSAGE =
  '업체는 등록됐지만 코드를 받지 못해 상세를 붙이지 못했습니다. 목록에 업체가 나타나면 [수정]으로 상세를 등록해 주세요.';
const IMAGE_OVERSIZE_MESSAGE = `이미지는 장당 ${Math.floor(MAX_IMAGE_BYTES / (1024 * 1024))}MB까지 올릴 수 있습니다.`;
const IMAGES_TOO_MANY_MESSAGE = `추가 이미지는 최대 ${MAX_EXTRA_IMAGES}장까지입니다.`;
const REQUEST_OVERSIZE_MESSAGE = `한 번에 올리는 이미지의 합이 ${Math.floor(MAX_ATTACHMENT_BYTES / (1024 * 1024))}MB를 넘습니다. 나눠서 저장해 주세요.`;

class InputError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'InputError';
  }
}

export type MerchantDetailActionResult =
  | { status: 'success' }
  | { status: 'error'; message: string };

function readString(formData: FormData, key: string): string {
  const value = formData.get(key);
  return typeof value === 'string' ? value : '';
}

function readOptionalInteger(formData: FormData, key: string): number | null {
  const raw = readString(formData, key).trim();
  if (raw === '') return null;
  const parsed = Number(raw);
  return Number.isFinite(parsed) ? parsed : Number.NaN;
}

function readFiles(formData: FormData, key: string): Array<File | null> {
  return formData
    .getAll(key)
    .map((entry) => (entry instanceof File && entry.size > 0 ? entry : null));
}

function assertImage(file: File): void {
  if (file.size > MAX_IMAGE_BYTES) {
    throw new InputError(IMAGE_OVERSIZE_MESSAGE);
  }
  if (!isAllowedFileExtension(FILE_MODULE.merchantImage, file.name)) {
    throw new InputError(
      `이미지는 ${allowedExtensions(FILE_MODULE.merchantImage).join(', ')} 파일만 올릴 수 있습니다.`
    );
  }
}

function failureMessage(error: unknown, fallback: string): string {
  const showable = error instanceof InputError || error instanceof BackendRequestError;
  return showable && error.message ? error.message : fallback;
}

type BatchFailure = { failedCount: number; error: unknown };

/**
 * 여러 건을 동시에 보내고 실패 수와 첫 오류를 돌려준다. 백엔드 관리자 API에는 트랜잭션이 없어
 * 한 건이 실패해도 나머지는 그대로 반영된다 — 차례로 보내며 멈추는 것보다 동시에 보내고 결과를
 * 세어 알리는 편이 빠르고 정확하다.
 */
async function runForEach(
  targets: readonly string[],
  run: (frcsNo: string) => Promise<void>
): Promise<BatchFailure | null> {
  const results = await Promise.allSettled(targets.map((frcsNo) => run(frcsNo)));
  const rejected = results.filter(
    (result): result is PromiseRejectedResult => result.status === 'rejected'
  );
  if (rejected.length === 0) return null;
  unstable_rethrow(rejected[0].reason);
  return { failedCount: rejected.length, error: rejected[0].reason };
}

function batchFailureMessage(failure: BatchFailure, total: number, fallback: string): string {
  const reason = failureMessage(failure.error, fallback);
  return total > 1 ? `${total}건 중 ${failure.failedCount}건 실패 — ${reason}` : reason;
}

// ── 상세 ──────────────────────────────────────────────────────────────────────

async function resolveThumbnailId(formData: FormData): Promise<string> {
  const file = readFiles(formData, 'thumbnailFile')[0] ?? null;
  if (!file) {
    return readString(formData, 'thumbnailFileId');
  }
  assertImage(file);
  return uploadAttachment(file, FILE_MODULE.merchantImage.id);
}

/**
 * 저장된 이미지 한 장을 다시 파일로 — 묶음을 새 순서로 다시 올릴 때 쓴다.
 *
 * 파일 이름은 원본 이름(묶음 파일 목록)을 쓴다 — 백엔드가 이름의 확장자로 허용 여부를 가르기 때문이다.
 * 목록에 없으면 MIME에서 만들되 `;charset=…` 같은 매개변수는 뗀다(안 떼면 확장자가
 * `jpeg;charset=utf-8`이 되어 거절당한다 — 실제로 났던 500).
 */
async function fetchSavedImage(
  fileId: string,
  fileSn: number,
  saved: AttachmentFile | undefined
): Promise<File> {
  const url = buildFileImageUrl(fileId, fileSn);
  if (!url) {
    throw new InputError(INVALID_REQUEST_MESSAGE);
  }
  const response = await fetch(url, { cache: 'no-store' });
  const blob = response.ok ? await response.blob() : null;
  // 파일이 없어도 백엔드가 200에 빈 본문으로 답한다(보고함) — 빈 파일을 다시 올리면 500이 난다.
  if (!blob || blob.size === 0) {
    throw new InputError(
      `저장된 추가 이미지 ${fileSn}번을 다시 읽지 못했습니다. 이 서버에 파일이 없을 수 있으니 이미지를 다시 올려 주세요.`
    );
  }
  const mimeType = blob.type.split(';')[0].trim();
  const extension = saved?.extension || mimeType.split('/')[1] || 'jpg';
  const name = saved?.name && /\.[A-Za-z0-9]+$/.test(saved.name) ? saved.name : `image-${fileSn}.${extension}`;
  return new File([blob], name, { type: mimeType || `image/${extension}` });
}

/**
 * 추가 이미지 묶음. 백엔드가 한 `atchFileId` 아래 `fileSn` 1..n으로 채번한다.
 * - 저장된 장이 그대로 앞에 있고 새 장이 뒤에만 붙었으면 기존 묶음에 덧붙인다(백엔드 40f85c4) —
 *   저장된 장을 다시 읽지 않아 빠르고, 파일이 다른 서버에 있어도 된다.
 * - 순서를 바꾸거나 저장된 장을 지웠으면 묶음을 통째로 다시 올린다 — 저장된 장은 백엔드에서 다시 읽는다.
 *
 * 폼: 칸마다 `imageSlot`(`saved:<fileSn>` | `new`)과 `imageFile`(같은 순서, 안 고른 칸은 빈 파일).
 */
async function resolveImageBundleId(formData: FormData): Promise<string> {
  const retainedId = readString(formData, 'imageFileId');
  const slots = formData
    .getAll('imageSlot')
    .map((entry) => (typeof entry === 'string' ? entry : ''));
  const files = readFiles(formData, 'imageFile');

  if (slots.length > MAX_EXTRA_IMAGES) {
    throw new InputError(IMAGES_TOO_MANY_MESSAGE);
  }

  const savedSns = slots
    .map((slot, index) =>
      files[index] === null && slot.startsWith('saved:') ? Number(slot.slice(6)) : null
    )
    .filter((sn): sn is number => sn !== null);
  const hasNewFile = files.some((file) => file !== null);
  const untouched =
    !hasNewFile &&
    savedSns.length === slots.length &&
    savedSns.every((sn, index) => sn === index + 1);

  if (slots.length === 0) {
    return '';
  }
  if (untouched) {
    return retainedId;
  }

  const appendOnly =
    retainedId !== '' &&
    savedSns.length > 0 &&
    savedSns.every((sn, index) => sn === index + 1) &&
    slots.every((slot, index) =>
      index < savedSns.length ? files[index] === null && slot === `saved:${index + 1}` : files[index] !== null
    );
  if (appendOnly) {
    const appended = files.slice(savedSns.length).filter((file): file is File => file !== null);
    appended.forEach(assertImage);
    const appendedBytes = appended.reduce((total, file) => total + file.size, 0);
    if (appendedBytes > MAX_ATTACHMENT_BYTES) {
      throw new InputError(REQUEST_OVERSIZE_MESSAGE);
    }
    return uploadAttachments(appended, FILE_MODULE.merchantImage.id, retainedId);
  }

  let newBytes = 0;
  const bundle: File[] = [];
  const savedFiles = retainedId && savedSns.length > 0 ? await fetchAttachmentFiles(retainedId) : [];
  for (const [index, slot] of slots.entries()) {
    const picked = files[index];
    if (picked) {
      assertImage(picked);
      newBytes += picked.size;
      bundle.push(picked);
    } else if (slot.startsWith('saved:') && retainedId) {
      const fileSn = Number(slot.slice(6));
      bundle.push(
        await fetchSavedImage(retainedId, fileSn, savedFiles.find((file) => file.fileSn === fileSn))
      );
    }
  }
  if (newBytes > MAX_ATTACHMENT_BYTES) {
    throw new InputError(REQUEST_OVERSIZE_MESSAGE);
  }
  if (bundle.length === 0) {
    return '';
  }
  return uploadAttachments(bundle, FILE_MODULE.merchantImage.id);
}

function readMerchantValues(formData: FormData, prefix = ''): MerchantDetailMerchantValues {
  return {
    name: readString(formData, `${prefix}name`),
    phoneNumber: readString(formData, `${prefix}phoneNumber`),
    postalCode: readString(formData, `${prefix}postalCode`),
    address: readString(formData, `${prefix}address`),
    addressDetail: readString(formData, `${prefix}addressDetail`),
  };
}

function isSameMerchant(a: MerchantDetailMerchantValues, b: MerchantDetailMerchantValues): boolean {
  return (
    a.name.trim() === b.name.trim() &&
    a.phoneNumber.trim() === b.phoneNumber.trim() &&
    a.postalCode.trim() === b.postalCode.trim() &&
    a.address.trim() === b.address.trim() &&
    a.addressDetail.trim() === b.addressDetail.trim()
  );
}

/** 상세 화면이 받는 업체 항목을 가맹점 등록·수정 요청 값으로 — 나머지는 이 화면에 없어 비운다. */
function toMerchantValues(frcsNo: string, values: MerchantDetailMerchantValues): MerchantValues {
  return {
    id: frcsNo,
    brandName: '',
    memo: '',
    name: values.name,
    categoryCode: '',
    businessNumber: '',
    ownerName: '',
    phoneNumber: values.phoneNumber,
    address: values.address,
    addressDetail: values.addressDetail,
    postalCode: values.postalCode,
    latitude: '',
    longitude: '',
    bankHolderName: '',
    bankCode: '',
    accountNumber: '',
    businessStatusCode: '',
    divisionCode: DEFAULT_MERCHANT_DIVISION_CODE,
    isVisible: true,
  };
}

/**
 * 업체(가맹점)를 만들거나 고치고 코드를 돌려준다.
 * - 코드가 없으면 새로 만든다 — 응답에 코드가 없으면(BizPlay 위임 응답) 목록에서 이름으로 찾는다.
 * - 코드가 있으면 값이 바뀌었을 때만 고친다 — BizPlay 위임 상태에선 불필요한 외부 호출을 피한다.
 */
async function saveMerchant(
  frcsNo: string,
  values: MerchantDetailMerchantValues,
  original: MerchantDetailMerchantValues
): Promise<string> {
  if (frcsNo) {
    if (!isSameMerchant(values, original)) {
      await updateMerchant(frcsNo, toMerchantValues(frcsNo, values));
    }
    return frcsNo;
  }
  const created = await createMerchant(toMerchantValues('', values));
  if (created) return created;
  const found = await findMerchantByName(values.name, values.phoneNumber);
  if (!found) {
    throw new InputError(MERCHANT_NOT_FOUND_AFTER_CREATE_MESSAGE);
  }
  return found.id;
}

function readDetailValues(
  formData: FormData,
  thumbnailFileId: string,
  imageFileId: string
): MerchantDetailValues {
  return {
    ...readMerchantValues(formData),
    frcsNo: readString(formData, 'frcsNo'),
    categoryCode: readString(formData, 'categoryCode'),
    description: readString(formData, 'description'),
    openTime: readString(formData, 'openTime'),
    closeTime: readString(formData, 'closeTime'),
    closedDays: readString(formData, 'closedDays'),
    isVisible: readString(formData, 'isVisible') !== 'N',
    isVoucherUsable: readString(formData, 'isVoucherUsable') !== 'N',
    sortOrder: readOptionalInteger(formData, 'sortOrder'),
    thumbnailFileId,
    imageFileId,
  };
}

export async function saveMerchantDetailAction(
  _prevState: MerchantDetailFormState,
  formData: FormData
): Promise<MerchantDetailFormState> {
  await verifySession();

  const isEdit = readString(formData, 'mode') === 'edit';
  let frcsNo = readString(formData, 'frcsNo');

  let failed: MerchantDetailFormState | null = null;
  try {
    const [thumbnailFileId, imageFileId, categories] = await Promise.all([
      resolveThumbnailId(formData),
      resolveImageBundleId(formData),
      fetchCommonCodes(CODE_GROUP.merchantType),
    ]);

    const validation = validateMerchantDetail(
      readDetailValues(formData, thumbnailFileId, imageFileId),
      categories
    );
    if (!validation.ok) {
      return { status: 'error', errors: validation.errors };
    }

    // 업체 먼저(만들거나 고치고), 그 코드로 상세를 붙인다.
    frcsNo = await saveMerchant(frcsNo, validation.values, readMerchantValues(formData, 'original'));
    const values = { ...validation.values, frcsNo };
    if (isEdit) {
      await updateMerchantDetail(values);
    } else {
      await createMerchantDetail(values);
    }
  } catch (error) {
    unstable_rethrow(error);
    console.error('[merchant-details] 저장 실패', error);
    const isConflict = !isEdit && error instanceof BackendRequestError && error.code === 409;
    failed = {
      status: 'error',
      message: isConflict ? DETAIL_CONFLICT_MESSAGE : failureMessage(error, SAVE_FAILED_MESSAGE),
    };
  }
  if (failed) {
    return failed;
  }

  revalidatePath(MERCHANT_DETAILS_PATH);
  revalidatePath(merchantDetailPath(frcsNo));
  // 등록·수정이 한 화면이라 여기서 옮기지 않는다 — 화면이 결과를 보고 머물거나 주소만 바꾼다.
  return { status: 'success', frcsNo, created: !isEdit };
}

export async function deleteMerchantDetailAction(
  frcsNo: string
): Promise<MerchantDetailActionResult> {
  await verifySession();
  if (!frcsNo) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  try {
    await deleteMerchantDetail(frcsNo);
  } catch (error) {
    unstable_rethrow(error);
    console.error('[merchant-details] 삭제 실패', error);
    return { status: 'error', message: failureMessage(error, DELETE_FAILED_MESSAGE) };
  }

  revalidatePath(MERCHANT_DETAILS_PATH);
  return { status: 'success' };
}

export async function deleteMerchantDetailsAction(
  frcsNos: string[]
): Promise<MerchantDetailActionResult> {
  await verifySession();
  const targets = frcsNos.filter((value) => typeof value === 'string' && value !== '');
  if (targets.length === 0) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  const failure = await runForEach(targets, deleteMerchantDetail);
  revalidatePath(MERCHANT_DETAILS_PATH);
  if (failure) {
    console.error('[merchant-details] 일괄 삭제 실패', failure.error);
    return { status: 'error', message: batchFailureMessage(failure, targets.length, DELETE_FAILED_MESSAGE) };
  }
  return { status: 'success' };
}

async function toggleDetailVisibility(frcsNo: string, isVisible: boolean): Promise<void> {
  const detail = await fetchMerchantDetail(frcsNo, { withImages: false });
  // 대표이미지는 필수라 상세가 있으면 반드시 온다 — 없으면 백엔드가 상세를 돌려주지 않은 것이다.
  if (!detail || !detail.hasDetail || !detail.thumbnailFileId || !detail.categoryCode) {
    throw new InputError(DETAIL_NOT_LOADED_MESSAGE);
  }
  await updateMerchantDetail({
    frcsNo,
    name: detail.name,
    phoneNumber: detail.phoneNumber ?? '',
    postalCode: detail.postalCode ?? '',
    address: detail.address ?? '',
    addressDetail: detail.addressDetail ?? '',
    categoryCode: detail.categoryCode,
    description: detail.description ?? '',
    openTime: detail.openTime ?? '',
    closeTime: detail.closeTime ?? '',
    closedDays: detail.closedDays ?? '',
    isVisible,
    isVoucherUsable: true,
    sortOrder: detail.sortOrder,
    thumbnailFileId: detail.thumbnailFileId,
    imageFileId: detail.imageFileId ?? '',
  });
}

export async function updateMerchantDetailVisibilityAction(
  frcsNo: string,
  isVisible: boolean
): Promise<MerchantDetailActionResult> {
  await verifySession();
  if (!frcsNo) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  try {
    await toggleDetailVisibility(frcsNo, isVisible);
  } catch (error) {
    unstable_rethrow(error);
    console.error('[merchant-details] 노출 변경 실패', error);
    return { status: 'error', message: failureMessage(error, SAVE_FAILED_MESSAGE) };
  }

  revalidatePath(MERCHANT_DETAILS_PATH);
  revalidatePath(merchantDetailPath(frcsNo));
  return { status: 'success' };
}

export async function deactivateMerchantDetailsAction(
  frcsNos: string[]
): Promise<MerchantDetailActionResult> {
  await verifySession();
  const targets = frcsNos.filter((value) => typeof value === 'string' && value !== '');
  if (targets.length === 0) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  const failure = await runForEach(targets, (frcsNo) => toggleDetailVisibility(frcsNo, false));
  revalidatePath(MERCHANT_DETAILS_PATH);
  if (failure) {
    console.error('[merchant-details] 일괄 비활성 실패', failure.error);
    return { status: 'error', message: batchFailureMessage(failure, targets.length, SAVE_FAILED_MESSAGE) };
  }
  return { status: 'success' };
}

// ── 메뉴 ──────────────────────────────────────────────────────────────────────

function readBadges(formData: FormData): MerchantProductBadge[] {
  return formData
    .getAll('badges')
    .map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
    .filter((value) => value !== '');
}

async function resolveProductImageId(formData: FormData): Promise<string> {
  const file = readFiles(formData, 'imageFile')[0] ?? null;
  if (!file) {
    return readString(formData, 'imageFileId');
  }
  assertImage(file);
  return uploadAttachment(file, FILE_MODULE.merchantImage.id);
}

export async function saveMerchantProductAction(
  _prevState: MerchantProductFormState,
  formData: FormData
): Promise<MerchantProductFormState> {
  await verifySession();

  const frcsNo = readString(formData, 'frcsNo');
  const productSn = readString(formData, 'productSn');
  if (!frcsNo) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  let failed: MerchantProductFormState | null = null;
  try {
    const [imageFileId, badgeCodes] = await Promise.all([
      resolveProductImageId(formData),
      fetchCommonCodes(CODE_GROUP.merchantProductBadge),
    ]);
    const validation = validateMerchantProduct(
      {
        frcsNo,
        name: readString(formData, 'name'),
        price: readOptionalInteger(formData, 'price'),
        description: readString(formData, 'description'),
        badges: readBadges(formData),
        isVisible: readString(formData, 'isVisible') !== 'N',
        sortOrder: readOptionalInteger(formData, 'sortOrder'),
        imageFileId,
      },
      badgeCodes
    );
    if (!validation.ok) {
      return { status: 'error', errors: validation.errors };
    }

    if (productSn) {
      await updateMerchantProduct(productSn, validation.values);
    } else {
      await createMerchantProduct(validation.values);
    }
  } catch (error) {
    unstable_rethrow(error);
    console.error('[merchant-products] 저장 실패', error);
    failed = { status: 'error', message: failureMessage(error, SAVE_FAILED_MESSAGE) };
  }
  if (failed) {
    return failed;
  }

  revalidatePath(merchantDetailPath(frcsNo));
  redirect(merchantDetailPath(frcsNo));
}

export async function deleteMerchantProductAction(
  frcsNo: string,
  productSn: string
): Promise<MerchantDetailActionResult> {
  await verifySession();
  if (!frcsNo || !productSn) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  try {
    await deleteMerchantProduct(productSn);
  } catch (error) {
    unstable_rethrow(error);
    console.error('[merchant-products] 삭제 실패', error);
    return { status: 'error', message: failureMessage(error, DELETE_FAILED_MESSAGE) };
  }

  revalidatePath(merchantDetailPath(frcsNo));
  return { status: 'success' };
}

export async function updateMerchantProductVisibilityAction(
  frcsNo: string,
  productSn: string,
  isVisible: boolean
): Promise<MerchantDetailActionResult> {
  await verifySession();
  if (!frcsNo || !productSn) {
    return { status: 'error', message: INVALID_REQUEST_MESSAGE };
  }

  try {
    const product = await fetchMerchantProduct(productSn);
    if (!product) {
      return { status: 'error', message: INVALID_REQUEST_MESSAGE };
    }
    await updateMerchantProduct(productSn, {
      frcsNo: product.frcsNo,
      name: product.name,
      price: product.price,
      description: product.description ?? '',
      badges: product.badges,
      isVisible,
      sortOrder: product.sortOrder,
      imageFileId: product.imageFileId ?? '',
    });
  } catch (error) {
    unstable_rethrow(error);
    console.error('[merchant-products] 노출 변경 실패', error);
    return { status: 'error', message: failureMessage(error, SAVE_FAILED_MESSAGE) };
  }

  revalidatePath(merchantDetailPath(frcsNo));
  return { status: 'success' };
}
