From f769c508adfba6548b6198213c32033e8a070286 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 27 Feb 2026 13:43:57 +0900 Subject: [PATCH] =?UTF-8?q?[as]=20feat:=20=EC=84=A4=EC=B9=98=EC=9D=B8?= =?UTF-8?q?=EA=B0=80=20=EB=AA=A9=EB=A1=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EB=B7=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/_template/app/layout.tsx | 17 +- apps/_template/src/app/styles/globals.css | 2 +- apps/_template/tailwind.config.ts | 3 - .../(with-base-layout)/instl-sncn/page.tsx | 1 + apps/as/app/(with-base-layout)/page.tsx | 1 - apps/as/app/layout.tsx | 13 +- apps/as/package.json | 1 + apps/as/pages/.gitkeep | 0 apps/as/src/app/providers/index.tsx | 4 +- apps/as/src/app/styles/components.css | 129 +++++++++++++ apps/as/src/app/styles/globals.css | 2 +- apps/as/src/pages/instl-sncn/index.ts | 1 + .../pages/instl-sncn/model/table-columns.tsx | 80 ++++++++ apps/as/src/pages/instl-sncn/ui/list-page.tsx | 178 ++++++++++++++++++ apps/as/tailwind.config.ts | 9 +- pnpm-lock.yaml | 9 + 16 files changed, 414 insertions(+), 36 deletions(-) create mode 100644 apps/as/app/(with-base-layout)/instl-sncn/page.tsx delete mode 100644 apps/as/app/(with-base-layout)/page.tsx create mode 100644 apps/as/pages/.gitkeep create mode 100644 apps/as/src/app/styles/components.css create mode 100644 apps/as/src/pages/instl-sncn/index.ts create mode 100644 apps/as/src/pages/instl-sncn/model/table-columns.tsx create mode 100644 apps/as/src/pages/instl-sncn/ui/list-page.tsx diff --git a/apps/_template/app/layout.tsx b/apps/_template/app/layout.tsx index 5f5f63ad8..a6eecfc27 100644 --- a/apps/_template/app/layout.tsx +++ b/apps/_template/app/layout.tsx @@ -1,15 +1,8 @@ import type { Metadata } from "next"; -import { Noto_Sans_KR } from "next/font/google"; +import { cookies } from "next/headers"; import { Providers } from "@/app/providers"; import "@/app/styles/globals.css"; -const notoSansKR = Noto_Sans_KR({ - subsets: ["latin"], - weight: ["100", "300", "400", "500", "700", "900"], - display: "swap", - variable: "--font-noto-sans-kr", -}); - export const metadata: Metadata = { title: "행정지원시스템", description: "행정지원시스템", @@ -19,10 +12,12 @@ interface RootLayoutProps { children: React.ReactNode; } -export default function RootLayout({ children }: RootLayoutProps) { +export default async function RootLayout({ children }: RootLayoutProps) { + const theme = (await cookies()).get("site-theme-key"); + return ( - - + + {children} diff --git a/apps/_template/src/app/styles/globals.css b/apps/_template/src/app/styles/globals.css index 60bc92fb6..e557f947b 100644 --- a/apps/_template/src/app/styles/globals.css +++ b/apps/_template/src/app/styles/globals.css @@ -10,6 +10,6 @@ html, body { color: var(--foreground); background: var(--background); - font-family: var(--font-noto-sans-kr), sans-serif; + font-family: "Noto Sans KR", sans-serif; font-size: 10px; } diff --git a/apps/_template/tailwind.config.ts b/apps/_template/tailwind.config.ts index c02405aca..d19dec7ed 100644 --- a/apps/_template/tailwind.config.ts +++ b/apps/_template/tailwind.config.ts @@ -18,9 +18,6 @@ const config: Config = { background: "var(--background)", foreground: "var(--foreground)", }, - fontFamily: { - sans: ["var(--font-noto-sans-kr)", "Noto Sans KR", "sans-serif"], - }, }, }, plugins: [], diff --git a/apps/as/app/(with-base-layout)/instl-sncn/page.tsx b/apps/as/app/(with-base-layout)/instl-sncn/page.tsx new file mode 100644 index 000000000..324982f41 --- /dev/null +++ b/apps/as/app/(with-base-layout)/instl-sncn/page.tsx @@ -0,0 +1 @@ +export { InstlSncnListPage as default } from "@/pages/instl-sncn"; diff --git a/apps/as/app/(with-base-layout)/page.tsx b/apps/as/app/(with-base-layout)/page.tsx deleted file mode 100644 index 82a918b59..000000000 --- a/apps/as/app/(with-base-layout)/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { MainPage as default } from "@/pages/home"; diff --git a/apps/as/app/layout.tsx b/apps/as/app/layout.tsx index 05858ffe8..5293046d6 100644 --- a/apps/as/app/layout.tsx +++ b/apps/as/app/layout.tsx @@ -1,17 +1,10 @@ import type { Metadata } from "next"; import { cookies } from "next/headers"; -import { Noto_Sans_KR } from "next/font/google"; import { Providers } from "@/app/providers"; import { setCookie } from "@/shared/lib"; import { SITE_ID } from "@/shared/config"; import "@/app/styles/globals.css"; - -const notoSansKR = Noto_Sans_KR({ - subsets: ["latin"], - weight: ["100", "300", "400", "500", "700", "900"], - display: "swap", - variable: "--font-noto-sans-kr", -}); +import "@/app/styles/components.css"; export const metadata: Metadata = { title: "행정지원시스템", @@ -28,8 +21,8 @@ export default async function RootLayout({ children }: RootLayoutProps) { const theme = (await cookies()).get("site-theme-key"); return ( - - + + {children} diff --git a/apps/as/package.json b/apps/as/package.json index 3806f73ee..92b2e6482 100644 --- a/apps/as/package.json +++ b/apps/as/package.json @@ -13,6 +13,7 @@ "@common/assets": "workspace:*", "@common/components": "workspace:*", "@common/network": "workspace:*", + "@faker-js/faker": "^10.3.0", "@hookform/resolvers": "^4.1.3", "@tanstack/react-query": "^5.75.5", "@tanstack/react-query-devtools": "^5.75.5", diff --git a/apps/as/pages/.gitkeep b/apps/as/pages/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/as/src/app/providers/index.tsx b/apps/as/src/app/providers/index.tsx index bc12c443b..ff37c57b7 100644 --- a/apps/as/src/app/providers/index.tsx +++ b/apps/as/src/app/providers/index.tsx @@ -12,7 +12,9 @@ export function Providers({ children }: ProvidersProps) { return ( {children} - +
+ +
); } diff --git a/apps/as/src/app/styles/components.css b/apps/as/src/app/styles/components.css new file mode 100644 index 000000000..5b9742c0f --- /dev/null +++ b/apps/as/src/app/styles/components.css @@ -0,0 +1,129 @@ +.card { + position: relative; + margin: 0.6rem; + box-sizing: border-box; + min-height: 2rem; + border-radius: 0.5rem; + padding: 0.6rem; + font-size: 1.3rem; + color: #666; +} + +.card_width10 { + width: calc(10% - 12px); +} + +.card_width20 { + width: calc(20% - 12px); +} + +.card_width25 { + width: calc(25% - 12px); +} + +.card_width30 { + width: calc(30% - 12px); +} + +.card_width33 { + width: calc(33.333% - 12px); +} + +.card_width40 { + width: calc(40% - 12px); +} + +.card_width45 { + width: calc(45% - 12px); +} + +.card_width50 { + width: calc(50% - 12px); +} + +.card_width60 { + width: calc(60% - 12px); +} + +.card_width66 { + width: calc(66.666% - 12px); +} + +.card_width70 { + width: calc(70% - 12px); +} + +.card_width75 { + width: calc(75% - 12px); +} + +.card_width80 { + width: calc(80% - 12px); +} + +.card_width90 { + width: calc(90% - 12px); +} + +.card_width100 { + width: calc(100% - 12px); +} + +.card_nopadding { + padding: 0 !important; +} + +.card_nomargin { + width: 100%; + margin: 0 !important; + padding: 0 !important; +} + +.topline { + border-top: 1px solid #ccc; +} + +.bottomline { + border-bottom: 1px solid #e6eef3; + background: #fff; +} + +.card-border { + border: 1px solid #dbe2e6; + background-color: rgba(255, 255, 255, 0.6); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +.card-header { + margin-top: -0.6rem; + margin-left: -0.6rem; + margin-right: -0.6rem; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid #e1e7eb; + padding: 1rem; +} + +.card-footer { + margin-left: -0.6rem; + margin-right: -0.6rem; + margin-top: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + border-top: 1px solid #e1e7eb; + padding-top: 1rem; + padding-bottom: 1rem; +} + +/* 블랙 테마 */ +[data-theme="black"] { + .card-border { + background-color: rgba(255, 255, 255, 0.065); + border-color: #38383a; + } + .card-header { + border-bottom-color: #3f4042; + } +} diff --git a/apps/as/src/app/styles/globals.css b/apps/as/src/app/styles/globals.css index 2b1b139cf..91c6845e5 100644 --- a/apps/as/src/app/styles/globals.css +++ b/apps/as/src/app/styles/globals.css @@ -10,6 +10,6 @@ html, body { color: var(--foreground); background: var(--background); - font-family: var(--font-noto-sans-kr), sans-serif; + font-family: "Noto Sans KR", sans-serif; font-size: 10px; } diff --git a/apps/as/src/pages/instl-sncn/index.ts b/apps/as/src/pages/instl-sncn/index.ts new file mode 100644 index 000000000..b9127c3a1 --- /dev/null +++ b/apps/as/src/pages/instl-sncn/index.ts @@ -0,0 +1 @@ +export { InstlSncnListPage } from "./ui/list-page"; diff --git a/apps/as/src/pages/instl-sncn/model/table-columns.tsx b/apps/as/src/pages/instl-sncn/model/table-columns.tsx new file mode 100644 index 000000000..e53a7fad4 --- /dev/null +++ b/apps/as/src/pages/instl-sncn/model/table-columns.tsx @@ -0,0 +1,80 @@ +import { createColumnHelper } from "@tanstack/react-table"; +import type { FnrscVO } from "@/entities/instl-sncn"; +import { Badge } from "@common/components"; + +const columnHelper = createColumnHelper(); + +const LIST_STEPS = ["저장", "신청완료", "기술검토", "검토결과 회신"] as const; + +function stepIndex(status: string | undefined): number { + if (!status) return 0; + const map: Record = { + SV: 0, + ST: 1, + LE: 2, + SL: 2, + ER: 2, + TE: 2, + TR: 2, + TP: 2, + EE: 2, + TA: 3, + }; + return map[status] ?? 0; +} + +function ProgrsSttusCell({ value }: { value?: string }) { + const current = stepIndex(value); + return ( +
+ {LIST_STEPS.map((label, i) => ( + <> + + {label} + + {i < LIST_STEPS.length - 1 && ( + + )} + + ))} +
+ ); +} + +export const instlSncnListColumns = [ + columnHelper.accessor("adstDstrNm", { + id: "adstDstrNm", + header: "지자체", + cell: (info) => info.getValue() ?? "-", + }), + columnHelper.accessor("bzNm", { + id: "bzNm", + header: "사업명", + cell: (info) => info.getValue() ?? "-", + }), + columnHelper.accessor("jobSeNm", { + id: "jobSeNm", + header: "업무구분", + cell: (info) => info.getValue() ?? "-", + }), + columnHelper.accessor("regId", { + id: "regId", + header: "담당자", + cell: (info) => info.getValue() ?? "-", + }), + columnHelper.accessor("rgnNm", { + id: "rgnNm", + header: "해당기관", + cell: (info) => info.getValue() ?? "-", + }), + columnHelper.display({ + id: "progrsSttus", + header: "진행상황", + cell: (info) => , + }), + columnHelper.accessor("regDt", { + id: "regDt", + header: "시행일", + cell: (info) => info.getValue() ?? "-", + }), +]; diff --git a/apps/as/src/pages/instl-sncn/ui/list-page.tsx b/apps/as/src/pages/instl-sncn/ui/list-page.tsx new file mode 100644 index 000000000..6aecea351 --- /dev/null +++ b/apps/as/src/pages/instl-sncn/ui/list-page.tsx @@ -0,0 +1,178 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { fakerKO as faker } from "@faker-js/faker"; +import { + Button, + DataTable, + Input, + Label, + LoadingWithAnimation, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; +import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useListPageLoadCnfm } from "@/entities/instl-sncn"; +import type { FnrscVO } from "@/entities/instl-sncn"; +import { instlSncnListColumns } from "../model/table-columns"; + +const PAGE_SIZE = 10; +const JOB_SE_OPTIONS = [ + { value: "ALL", label: "전체" }, + { value: "SF", label: "하수처리시설" }, + { value: "SP", label: "하수관로" }, + { value: "RE", label: "재이용수" }, +] as const; + +function buildMockFnrscVO(): FnrscVO { + return { + instlCnfmDocNo: faker.string.alphanumeric(10), + adstDstrNm: faker.location.city(), + bzNm: faker.company.name(), + jobSeNm: faker.helpers.arrayElement(["하수처리시설", "하수관로", "재이용수"]), + regId: faker.person.fullName(), + rgnNm: faker.company.name(), + progrsSttus: faker.helpers.arrayElement(["SV", "ST", "SL", "ER", "EE", "PR", "PE", "AP", "RJ"]), + regDt: faker.date.past().toISOString().slice(0, 10), + }; +} + +const MOCK_LIST: FnrscVO[] = Array.from({ length: PAGE_SIZE }, () => buildMockFnrscVO()); + +export function InstlSncnListPage() { + const router = useRouter(); + const [bzNm, setBzNm] = useState(""); + const [jobSeNm, setJobSeNm] = useState("ALL"); + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: PAGE_SIZE }); + + const searchVO: FnrscVO = { + bzNm: bzNm.trim() || undefined, + jobSeNm: jobSeNm === "ALL" ? undefined : jobSeNm, + pageIndex: pagination.pageIndex + 1, + recordCountPerPage: PAGE_SIZE, + pageSize: PAGE_SIZE, + }; + + const { data, isPending, isError } = useListPageLoadCnfm(searchVO); + const list = data?.list && data.list.length > 0 ? data.list : MOCK_LIST; + const totalCount = data?.cnt ?? list.length; + const pageCount = data?.paginationInfo?.lastPageNo ?? 1; + + const handleSearch = useCallback((e: React.FormEvent) => { + e.preventDefault(); + setPagination((p) => ({ ...p, pageIndex: 0 })); + }, []); + + const handleRowClick = useCallback( + (row: FnrscVO) => { + if (row.instlCnfmDocNo) { + router.push(`/instl-sncn/${row.instlCnfmDocNo}`); + } + }, + [router], + ); + + const handleRegist = useCallback(() => { + router.push("/instl-sncn/regist"); + }, [router]); + + const table = useReactTable({ + data: list, + columns: instlSncnListColumns, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount, + state: { + pagination, + }, + onPaginationChange: setPagination, + }); + + const isEmpty = list.length === 0; + + return ( +
+
+
+ + setBzNm(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSearch(e as unknown as React.FormEvent)} + /> +
+
+ + +
+ +
+ +
+
+

+ 검색된 리스트 ({totalCount}) +

+ +
+ +
+ {isPending && ( +
+
+ +
+
+ )} + {/* {isError && ( +
+ 목록을 불러오는 중 오류가 발생했습니다. +
+ )} */} + {!isPending && !isError && isEmpty && ( +
+ 목록이 존재하지 않습니다. +
+ )} + {!isPending && !isEmpty && ( +
+ +
+ )} +
+
+
+ ); +} diff --git a/apps/as/tailwind.config.ts b/apps/as/tailwind.config.ts index c02405aca..e51b76dd9 100644 --- a/apps/as/tailwind.config.ts +++ b/apps/as/tailwind.config.ts @@ -4,12 +4,8 @@ import presetConfig from "../../tailwind.config.preset"; const config: Config = { presets: [presetConfig], content: [ - "./pages/**/*.{js,ts,jsx,tsx,mdx}", - "./shared/**/*.{js,ts,jsx,tsx,mdx}", - "./entities/**/*.{js,ts,jsx,tsx,mdx}", - "./features/**/*.{js,ts,jsx,tsx,mdx}", - "./widgets/**/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}", + "./src/**/*.{js,ts,jsx,tsx,mdx}", "../../packages/common/components/dist/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { @@ -18,9 +14,6 @@ const config: Config = { background: "var(--background)", foreground: "var(--foreground)", }, - fontFamily: { - sans: ["var(--font-noto-sans-kr)", "Noto Sans KR", "sans-serif"], - }, }, }, plugins: [], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7b788c19..b6572670c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,6 +126,9 @@ importers: '@common/network': specifier: workspace:* version: link:../../packages/common/network + '@faker-js/faker': + specifier: ^10.3.0 + version: 10.3.0 '@hookform/resolvers': specifier: ^4.1.3 version: 4.1.3(react-hook-form@7.68.0(react@19.2.3)) @@ -2476,6 +2479,10 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@faker-js/faker@10.3.0': + resolution: {integrity: sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw==} + engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} + '@faker-js/faker@9.9.0': resolution: {integrity: sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} @@ -7901,6 +7908,8 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@faker-js/faker@10.3.0': {} + '@faker-js/faker@9.9.0': {} '@feature-sliced/filesystem@3.0.1':