File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
'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 (
<span className={styles.truncate} title={text ?? undefined}>
{value}
</span>
);
}
/** 어느 팝업이 열려 있는지. 한 번에 하나만 열린다. */
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`이다 — 화면에 여럿 설 수 있는 구역 제목이라 `<h2>`로 나간다.
*
* 우측에는 검색이 없다(시안) — 상세코드는 선택된 그룹에 속한 것이 전부라 걸러 낼 대상이 아니다.
*/
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<OpenModal>(null);
const [isDeleting, startDeleting] = useTransition();
function go(patch: Partial<CommonCodeQuery>) {
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: (
<>
<FoxButton type="default" size="md" label="취소" onAction={hideAlert} />
<FoxButton
type="error"
size="md"
label="삭제"
onAction={() => {
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<CommonCodeGroup>[] = [
{
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) => <Truncated text={row.description} />,
},
{
key: 'createdAt',
header: '생성일',
width: 120,
render: (row) => formatOptionalText(row.createdAt),
},
{
key: 'actions',
header: '관리',
width: 120,
render: (row) => (
<CodeRowActions
target={row.comCd}
deleting={isDeleting}
onEdit={() => setModal({ kind: 'group-edit', group: row })}
onDelete={() =>
confirmDelete(
'공통코드를 삭제하시겠습니까?',
`${row.name}(${row.comCd})을(를) 삭제합니다. 속한 상세코드도 함께 쓸 수 없게 됩니다.`,
() => deleteCodeGroupAction(row.comCd)
)
}
/>
),
},
];
const detailColumns: FoxListColumn<CommonCodeDetail>[] = [
{
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) => <Truncated text={row.description} />,
},
{
key: 'createdAt',
header: '생성일',
width: 120,
render: (row) => formatOptionalText(row.createdAt),
},
{
key: 'actions',
header: '관리',
width: 120,
render: (row) => (
<CodeRowActions
target={row.comDtlCd}
deleting={isDeleting}
onEdit={() => setModal({ kind: 'detail-edit', detail: row })}
onDelete={() =>
confirmDelete(
'상세코드를 삭제하시겠습니까?',
`${row.name}(${row.comDtlCd})을(를) 삭제합니다. 삭제 후에는 되돌릴 수 없습니다.`,
() => deleteCodeDetailAction(row.comCd, row.comDtlCd)
)
}
/>
),
},
];
return (
<div className={styles.page}>
<FoxPageHeader
title="코드관리"
breadcrumb={[
{ label: '홈', href: '/' },
{ label: '시스템관리' },
{ label: '코드관리' },
]}
/>
<div className={styles.columns}>
<section className={styles.panel}>
<FoxHeadingGroup title="공통코드 목록" />
<FoxListContainer<CommonCodeGroup>
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={
<FoxButton
type="primary"
size="md"
leadingIcon={<FoxPlusIcon />}
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,
});
}
}}
/>
</section>
<section className={styles.panel}>
<FoxHeadingGroup title="상세코드 목록" />
<FoxListContainer<CommonCodeDetail>
caption="상세코드 목록"
columns={detailColumns}
rows={details}
rowKey={(row) => `${row.comCd}:${row.comDtlCd}`}
totalCount={detailTotalCount}
emptyMessage={
selectedComCd
? '등록된 상세코드가 없습니다.'
: '왼쪽에서 공통코드를 선택해 주세요.'
}
searchHidden
toolbarActions={
<FoxButton
type="primary"
size="md"
leadingIcon={<FoxPlusIcon />}
label="상세코드 등록"
// 소속될 공통코드가 없으면 등록할 수 없다.
disabled={!selectedComCd}
onAction={() => setModal({ kind: 'detail-create' })}
/>
}
page={detailPage}
totalPages={detailTotalPages}
buildHref={(patch) =>
buildCommonCodeHref(query, {
comCd: selectedComCd,
detailPage: Number(patch.page ?? 1),
})
}
/>
</section>
</div>
{modal?.kind === 'group-create' && (
<CodeGroupModal onClose={() => setModal(null)} />
)}
{modal?.kind === 'group-edit' && (
<CodeGroupModal group={modal.group} onClose={() => setModal(null)} />
)}
{modal?.kind === 'detail-create' && selectedComCd && (
<CodeDetailModal
comCd={selectedComCd}
nextSortSeq={nextSortSeq}
onClose={() => setModal(null)}
/>
)}
{modal?.kind === 'detail-edit' && (
<CodeDetailModal
comCd={modal.detail.comCd}
detail={modal.detail}
nextSortSeq={nextSortSeq}
onClose={() => setModal(null)}
/>
)}
</div>
);
}