'use client'; import { useRouter } from 'next/navigation'; import { useState } 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 { useFeedback } from '@/app/_hooks/use-feedback'; import { useBlockingAction } from '@/app/_hooks/use-blocking-action'; 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] = useBlockingAction(); 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={
caption="상세코드 목록" columns={detailColumns} rows={details} rowKey={(row) => `${row.comCd}:${row.comDtlCd}`} totalCount={detailTotalCount} emptyMessage={ selectedComCd ? '등록된 상세코드가 없습니다.' : '왼쪽에서 공통코드를 선택해 주세요.' } searchHidden toolbarActions={
{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)} /> )}
); }