'use client';
import { useRouter } from 'next/navigation';
import { useState, useTransition } from 'react';
import { FoxButton } from '@fox/core/components/fox-button';
import { FoxHeadingGroup } from '@fox/core/components/fox-heading-group';
import {
FoxListContainer,
type FoxListColumn,
} from '@fox/core/components/fox-list-container';
import { FoxPageHeader } from '@fox/core/components/fox-page-header';
import { FoxPlusIcon } from '@fox/core/icons';
import { useFeedback } from '@/app/_hooks/use-feedback';
import {
formatOptionalText,
type CommonCodeDetail,
type CommonCodeGroup,
} from '@/lib/domain/common-code';
import {
COMMON_CODE_PAGE_SIZE,
COMMON_CODE_SEARCH_FIELD_OPTIONS,
buildCommonCodeHref,
type CommonCodeQuery,
type CommonCodeSearchField,
} from '@/lib/domain/common-code-query';
import { deleteCodeDetailAction, deleteCodeGroupAction } from '../_actions';
import { CodeDetailModal } from './code-detail-modal';
import { CodeGroupModal } from './code-group-modal';
import { CodeRowActions } from './code-row-actions';
import styles from './code-management.module.scss';
interface CodeManagementProps {
query: CommonCodeQuery;
groups: CommonCodeGroup[];
groupTotalCount: number;
groupPage: number;
groupTotalPages: number;
/** 화면이 실제로 보여주는 선택 — URL 값이 목록에 없으면 서버가 첫 행으로 바꿔 넘긴다. */
selectedComCd: string | null;
details: CommonCodeDetail[];
detailTotalCount: number;
detailPage: number;
detailTotalPages: number;
nextSortSeq: number;
}
/**
* 설명 칸 — 한 줄로 줄이고 넘치면 말줄임한다(스타일 주석 참조). 잘린 글자를 읽을 방법이
* 있어야 하므로 전문을 `title`로 남긴다.
*/
function Truncated({ text }: { text: string | null }) {
const value = formatOptionalText(text);
return (
{value}
);
}
/** 어느 팝업이 열려 있는지. 한 번에 하나만 열린다. */
type OpenModal =
| { kind: 'group-create' }
| { kind: 'group-edit'; group: CommonCodeGroup }
| { kind: 'detail-create' }
| { kind: 'detail-edit'; detail: CommonCodeDetail }
| null;
/**
* 코드관리 — 기획 SYS_COD_001, 시안 관리자페이지(QsFVFFUKGH68xAPVkF3SJ9) 5402:13399
*
* 목록이 둘이지만 화면은 하나다. 좌측 공통코드를 **행 전체로 눌러** 고르면 우측 상세코드가 그
* 그룹으로 갱신된다(기획 ④) — 선택은 URL에 실리므로 갱신은 서버가 다시 그려 준다.
*
* 두 목록 모두 `FoxListContainer`를 쓰되 화면 제목은 위에서 한 번만 그린다(`FoxPageHeader`).
* 패널 이름은 `FoxHeadingGroup`이다 — 화면에 여럿 설 수 있는 구역 제목이라 `
`로 나간다.
*
* 우측에는 검색이 없다(시안) — 상세코드는 선택된 그룹에 속한 것이 전부라 걸러 낼 대상이 아니다.
*/
export function CodeManagement({
query,
groups,
groupTotalCount,
groupPage,
groupTotalPages,
selectedComCd,
details,
detailTotalCount,
detailPage,
detailTotalPages,
nextSortSeq,
}: CodeManagementProps) {
const router = useRouter();
const { showAlert, hideAlert, showToast } = useFeedback();
const [modal, setModal] = useState(null);
const [isDeleting, startDeleting] = useTransition();
function go(patch: Partial) {
router.replace(buildCommonCodeHref(query, patch));
}
/**
* 삭제는 되돌릴 수 없어 확인 얼럿을 거친다(기획 ④). 성공 뒤 화면 갱신은 Server Action의
* `revalidatePath`가 맡으므로 여기서는 알림만 띄운다.
*/
function confirmDelete(
title: string,
message: string,
run: () => Promise<{ status: string; message?: string }>
) {
showAlert({
variant: 'danger',
title,
message,
actions: (
<>
{
hideAlert();
startDeleting(async () => {
const result = await run();
if (result.status === 'error') {
showToast({
variant: 'danger',
message: result.message ?? '삭제하지 못했습니다.',
});
return;
}
showToast({ variant: 'success', message: '삭제했습니다.' });
});
}}
/>
>
),
});
}
const groupColumns: FoxListColumn[] = [
{
key: 'no',
header: '번호',
width: 80,
render: (_row, index) => (groupPage - 1) * COMMON_CODE_PAGE_SIZE + index + 1,
},
{ key: 'comCd', header: '코드ID', width: 120 },
{ key: 'name', header: '코드명', width: 160, emphasis: true },
{
key: 'description',
header: '코드설명',
width: 156,
render: (row) => ,
},
{
key: 'createdAt',
header: '생성일',
width: 120,
render: (row) => formatOptionalText(row.createdAt),
},
{
key: 'actions',
header: '관리',
width: 120,
render: (row) => (
setModal({ kind: 'group-edit', group: row })}
onDelete={() =>
confirmDelete(
'공통코드를 삭제하시겠습니까?',
`${row.name}(${row.comCd})을(를) 삭제합니다. 속한 상세코드도 함께 쓸 수 없게 됩니다.`,
() => deleteCodeGroupAction(row.comCd)
)
}
/>
),
},
];
const detailColumns: FoxListColumn[] = [
{
key: 'no',
header: '번호',
width: 80,
// 기획의 상세코드 목록은 이 자리에 정렬번호를 보여준다(FSC01→1, FSC10→10).
render: (row) => row.sortSeq,
},
{ key: 'comDtlCd', header: '상세코드ID', width: 120 },
{ key: 'name', header: '상세코드명', width: 120, emphasis: true },
{
key: 'description',
header: '상세코드설명',
width: 196,
render: (row) => ,
},
{
key: 'createdAt',
header: '생성일',
width: 120,
render: (row) => formatOptionalText(row.createdAt),
},
{
key: 'actions',
header: '관리',
width: 120,
render: (row) => (
setModal({ kind: 'detail-edit', detail: row })}
onDelete={() =>
confirmDelete(
'상세코드를 삭제하시겠습니까?',
`${row.name}(${row.comDtlCd})을(를) 삭제합니다. 삭제 후에는 되돌릴 수 없습니다.`,
() => deleteCodeDetailAction(row.comCd, row.comDtlCd)
)
}
/>
),
},
];
return (
caption="공통코드 목록"
columns={groupColumns}
rows={groups}
rowKey={(row) => row.comCd}
totalCount={groupTotalCount}
emptyMessage="조회된 코드가 없습니다. 다른 검색어로 다시 시도해 주세요."
searchFields={COMMON_CODE_SEARCH_FIELD_OPTIONS.map((option) => ({
value: option.value,
label: option.label,
}))}
searchField={query.searchField}
keyword={query.keyword}
toolbarActions={
}
label="신규 등록"
onAction={() => setModal({ kind: 'group-create' })}
/>
}
onRowSelect={(row) => go({ comCd: row.comCd })}
page={groupPage}
totalPages={groupTotalPages}
buildHref={(patch) =>
buildCommonCodeHref(query, { page: Number(patch.page ?? 1) })
}
onQueryChange={(patch) => {
if (patch.keyword !== undefined || patch.searchField !== undefined) {
go({
keyword: String(patch.keyword ?? ''),
searchField: (patch.searchField ??
query.searchField) as CommonCodeSearchField,
// 검색하면 목록 자체가 달라지므로 선택도 첫 행으로 다시 잡게 비운다.
comCd: null,
page: 1,
});
}
}}
/>
caption="상세코드 목록"
columns={detailColumns}
rows={details}
rowKey={(row) => `${row.comCd}:${row.comDtlCd}`}
totalCount={detailTotalCount}
emptyMessage={
selectedComCd
? '등록된 상세코드가 없습니다.'
: '왼쪽에서 공통코드를 선택해 주세요.'
}
searchHidden
toolbarActions={
}
label="상세코드 등록"
// 소속될 공통코드가 없으면 등록할 수 없다.
disabled={!selectedComCd}
onAction={() => setModal({ kind: 'detail-create' })}
/>
}
page={detailPage}
totalPages={detailTotalPages}
buildHref={(patch) =>
buildCommonCodeHref(query, {
comCd: selectedComCd,
detailPage: Number(patch.page ?? 1),
})
}
/>
{modal?.kind === 'group-create' && (
setModal(null)} />
)}
{modal?.kind === 'group-edit' && (
setModal(null)} />
)}
{modal?.kind === 'detail-create' && selectedComCd && (
setModal(null)}
/>
)}
{modal?.kind === 'detail-edit' && (
setModal(null)}
/>
)}
);
}