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 (
+
+
+
+
+
+
+ 검색된 리스트 ({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':