-
-
-
-
+
-
+
>
);
};
diff --git a/apps/st/src/components/statistics/verification/ManagementVerificationRule.tsx b/apps/st/src/components/statistics/verification/ManagementVerificationRule.tsx
new file mode 100644
index 000000000..040a68b51
--- /dev/null
+++ b/apps/st/src/components/statistics/verification/ManagementVerificationRule.tsx
@@ -0,0 +1,11 @@
+import SearchForm from "@/components/module/SearchForm";
+
+const ManagementVerificationRule = () => {
+ return (
+ <>
+
+ >
+ );
+};
+
+export default ManagementVerificationRule;
\ No newline at end of file
diff --git a/apps/st/src/components/util/column/review/SeweragePenetrationRateColumnRvw.ts b/apps/st/src/components/util/column/review/SeweragePenetrationRateColumnRvw.ts
index d792174b7..fe7869a48 100644
--- a/apps/st/src/components/util/column/review/SeweragePenetrationRateColumnRvw.ts
+++ b/apps/st/src/components/util/column/review/SeweragePenetrationRateColumnRvw.ts
@@ -1,95 +1,99 @@
-export const SeweragePenetrationRateInfoList = [
- {
- tabKey: 'a',
- tabLabel: '하수도 보급률 현황',
- rowStartNum: 3, // 수계영향권 및 처리구역 내/외 3단 헤더 구조
- fixedColumns: 2, // 순번, 시도, 구군까지 고정
- columns: [
- /* 1. 행정 및 수계 정보 */
- { name: 'sido', fieldName: 'SIDO', header: { text: '시도' }, editable: false },
- { name: 'sgg', fieldName: 'GUGUN', header: { text: '구군' }, editable: false },
- { name: 'GBN', fieldName: 'GBN', header: { text: '구분' }, editable: false },
- { name: 'WTR_SYS', fieldName: 'WTR_SYS', header: { text: '수계' }, editable: false },
- { name: 'bstrm', fieldName: 'BSTRM', header: { text: '지류' }, editable: false },
- { name: 'mdbsn', fieldName: 'MDBSN', header: { text: '세부단위유역' }, editable: false },
- { name: 'lgbsn', fieldName: 'LGBSN', header: { text: '중권역' }, editable: false },
- { name: 'mdmbsn', fieldName: 'MDMBSN', header: { text: '소권역' }, editable: false },
- { name: 'specl_cntrpln_ar', fieldName: 'SPECL_CNTRPLN_AR', header: { text: '특별대책지역' }, editable: false },
+import { ValueType } from "realgrid";
+import type { ColumnConfig } from "@rsone/components";
- /* 2. 인구 및 면적 총계 */
- { name: 'popltn_sm', fieldName: 'POPLTN_SM', header: { text: '총인구(명)' }, editable: false },
- { name: 'ar_sm', fieldName: 'AR_SM', header: { text: '총면적(㎢)' }, editable: false },
+export const SeweragePenetrationRateInfoList = {
+ columns: [
+ { name: 'sido', fieldName: '시도', header: { text: '시도' }, mergeCells: true, editable: false, sortdisable: true },
+ { name: 'sgg', fieldName: '시군구', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true },
+ { name: '구분', fieldName: '구분', header: { text: '구분' }, editable: false, sortdisable: true },
+ { name: '수계', fieldName: '수계', header: { text: '수계' } },
+ { name: 'bstrm', fieldName: '지류', header: { text: '지류' } },
+ { name: 'mdbsn', fieldName: '세부단위유역', header: { text: '세부단위유역' } },
+ { name: 'lgbsn', fieldName: '중권역', header: { text: '중권역' } },
+ { name: 'mdmbsn', fieldName: '소권역', header: { text: '소권역' } },
+ { name: 'specl_cntrpln_ar', fieldName: '특별대책지역', header: { text: '특별대책지역' } },
+ { name: 'total_popl', fieldName: '총인구', header: { text: '총인구' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
+ { name: 'popl_sm', fieldName: '내국인', header: { text: '내국인' } },
+ { name: 'frgnr_popl_sm', fieldName: '외국인', header: { text: '외국인' } },
+ { name: 'area', fieldName: '행정참고자료면적(㎢)', header: { text: '행정참고자료면적(㎢)' } },
+ { name: 'popltn_sm', fieldName: '총인구(명)', header: { text: '총인구(명)' } },
+ { name: 'ar_sm', fieldName: '총면적(㎢)', header: { text: '총면적(㎢)' }, valueType: ValueType.NUMBER },
+ { name: '계', fieldName: '계', header: { text: '계' } },
+ //공공하수처리인구
+ { name: '합계', fieldName: '합계', header: { text: '합계' } },
+ { name: '1차처리', fieldName: '1차처리', header: { text: '1차처리' } },
+ { name: '2차처리', fieldName: '2차처리', header: { text: '2차처리' } },
+ { name: '3차처리', fieldName: '3차처리', header: { text: '3차처리' } },
+ //폐수처리인구(명)
+ { name: '(폐수처리인구)합계', fieldName: '(폐수처리인구)합계', header: { text: '합계' } },
+ { name: '(폐수처리인구)1차처리', fieldName: '(폐수처리인구)1차처리', header: { text: '1차처리' } },
+ { name: '(폐수처리인구)2차처리', fieldName: '(폐수처리인구)2차처리', header: { text: '2차처리' } },
+ { name: '(폐수처리인구)3차처리', fieldName: '(폐수처리인구)3차처리', header: { text: '3차처리' } },
- /* 3. 하수처리구역 내 상세 */
- { name: 'inner_sm', fieldName: 'INNER_SM', header: { text: '계' }, editable: false },
- // 공공하수처리인구
- { name: 'pbl_sm', fieldName: 'PBL_SM', header: { text: '합계' }, editable: false },
- { name: 'pbl_1ch', fieldName: 'PBL_1CH', header: { text: '1차처리' }, editable: false },
- { name: 'pbl_2ch', fieldName: 'PBL_2CH', header: { text: '2차처리' }, editable: false },
- { name: 'pbl_3ch', fieldName: 'PBL_3CH', header: { text: '3차처리' }, editable: false },
- // 폐수처리인구
- { name: 'wstr_sm', fieldName: 'WSTR_SM', header: { text: '합계' }, editable: false },
- { name: 'wstr_1ch', fieldName: 'WSTR_1CH', header: { text: '1차처리' }, editable: false },
- { name: 'wstr_2ch', fieldName: 'WSTR_2CH', header: { text: '2차처리' }, editable: false },
- { name: 'wstr_3ch', fieldName: 'WSTR_3CH', header: { text: '3차처리' }, editable: false },
-
- { name: 'not_conect_popltn', fieldName: 'NOT_CONECT_POPLTN', header: { text: '미접속인구' }, editable: false },
- { name: 'swge_proc_zone_inner_ar', fieldName: 'SWGE_PROC_ZONE_INNER_AR', header: { text: '면적(㎢)' }, editable: false },
-
- /* 4. 하수처리구역 외 상세 */
- { name: 'swge_proc_zone_outer_sm', fieldName: 'SWGE_PROC_ZONE_OUTER_SM', header: { text: '계' }, editable: false },
- { name: 'town_unproc_popltn', fieldName: 'TOWN_UNPROC_POPLTN', header: { text: '미처리인구' }, editable: false },
- { name: 'town_swtt_popltn', fieldName: 'TOWN_SWTT_POPLTN', header: { text: '오수처리인구' }, editable: false },
- { name: 'town_wrrtn_popltn', fieldName: 'TOWN_WRRTN_POPLTN', header: { text: '정화조인구' }, editable: false },
- { name: 'swge_proc_zone_outer_ar', fieldName: 'SWGE_PROC_ZONE_OUTER_AR', header: { text: '면적(㎢)' }, editable: false },
-
- /* 5. 보급율 및 지표 */
- { name: 'pblswrg_popltn_suprt', fieldName: 'PBLSWRG_POPLTN_SUPRT', header: { text: '공공하수처리구역\n인구보급율(%)' }, editable: false },
- { name: 'adtrt_popltn_suprt', fieldName: 'ADTRT_POPLTN_SUPRT', header: { text: '고도처리\n인구보급율(%)' }, editable: false },
- { name: 'swrg_instlrt', fieldName: 'SWRG_INSTLRT', header: { text: '하수도설치율(%)' }, editable: false },
- { name: 'rmk', fieldName: 'RMK', header: { text: '비고' }, editable: false },
- ],
- columnLayout: [
- { name: 'groupA', header: { text: '행정구역' }, items: ['sido', 'sgg'] },
- 'GBN',
- 'WTR_SYS',
- 'bstrm',
- {
- name: 'groupB',
- header: { text: '수계영향권별분류' },
- items: ['mdbsn', 'lgbsn', 'mdmbsn'],
- },
- 'specl_cntrpln_ar',
- 'popltn_sm',
- 'ar_sm',
- {
- name: 'groupD',
- header: { text: '하수처리구역내' },
- items: [
- 'inner_sm',
- {
- name: 'pblswrgGroup',
- header: { text: '공공하수처리인구(명)' },
- items: ['pbl_sm', 'pbl_1ch', 'pbl_2ch', 'pbl_3ch'],
- },
- {
- name: 'wstrGroup',
- header: { text: '폐수처리인구(명)' },
- items: ['wstr_sm', 'wstr_1ch', 'wstr_2ch', 'wstr_3ch'],
- },
- 'not_conect_popltn',
- 'swge_proc_zone_inner_ar'
- ],
- },
- {
- name: 'groupE',
- header: { text: '하수처리구역외' },
- items: ['swge_proc_zone_outer_sm', 'town_unproc_popltn', 'town_swtt_popltn', 'town_wrrtn_popltn', 'swge_proc_zone_outer_ar'],
- },
- 'pblswrg_popltn_suprt',
- 'adtrt_popltn_suprt',
- 'swrg_instlrt',
- 'rmk',
- ]
- }
-];
\ No newline at end of file
+ { name: 'not_conect_popltn', fieldName: '미접속인구', header: { text: '미접속인구' } },
+ { name: 'swge_proc_zone_inner_ar', fieldName: '면적(㎢)', header: { text: '면적(㎢)' } },
+ { name: 'swge_proc_zone_outer_sm', fieldName: '합계', header: { text: '계' } },
+ { name: 'town_unproc_popltn', fieldName: '미처리인구', header: { text: '미처리인구' } },
+ { name: 'town_swtt_popltn', fieldName: '오수처리인구', header: { text: '오수처리인구' } },
+ { name: 'town_wrrtn_popltn', fieldName: '정화조인구', header: { text: '정화조인구' } },
+ { name: 'swge_proc_zone_outer_ar', fieldName: '면적(㎢)', header: { text: '면적(㎢)' } },
+ { name: 'pblswrg_popltn_suprt', fieldName: '공공하수처리구역인구보급율(%)', header: { text: '공공하수처리구역인구보급율(%)' } },
+ { name: 'adtrt_popltn_suprt', fieldName: '고도처리인구보급율(%)', header: { text: `고도처리인구보급율(%)` } },
+ { name: 'swrg_instlrt', fieldName: '하수도설치율(%)', header: { text: '하수도설치율(%)' } },
+ { name: 'rmk', fieldName: '비고', header: { text: '비고' } },
+ ] as ColumnConfig[],
+ columnLayout: [
+ {
+ name: 'groupA',
+ hideChildHeaders: false,
+ header: { text: '행정구역' },
+ items: ['sido', 'sgg'],
+ },
+ '구분',
+ '수계',
+ 'bstrm',
+ {
+ name: 'groupB',
+ hideChildHeaders: false,
+ header: { text: '수계영향권별분류류' },
+ items: ['mdbsn', 'lgbsn', 'mdmbsn'],
+ },
+ 'specl_cntrpln_ar',
+ 'popltn_sm',
+ 'ar_sm',
+ {
+ name: 'groupD',
+ hideChildHeaders: false,
+ header: { text: '하수처리구역내' },
+ items: [
+ '계',
+ {
+ name: 'pblswrgGroup',
+ hideChildHeaders: false,
+ header: { text: '공공하수처리인구(명)' },
+ items: ['합계', '1차처리', '2차처리', '3차처리'],
+ },
+ {
+ name: 'wstrGroup',
+ hideChildHeaders: false,
+ header: { text: '폐수처리인구(명)' },
+ items: ['(폐수처리인구)합계', '(폐수처리인구)1차처리', '(폐수처리인구)2차처리', '(폐수처리인구)3차처리'],
+ },
+ 'not_conect_popltn',
+ 'swge_proc_zone_inner_ar'
+ ],
+ },
+ {
+ name: 'groupE',
+ hideChildHeaders: false,
+ header: { text: '하수처리구역외' },
+ items: ['swge_proc_zone_outer_sm', 'town_unproc_popltn', 'town_swtt_popltn', 'town_wrrtn_popltn', 'swge_proc_zone_outer_ar'],
+ },
+ 'pblswrg_popltn_suprt',
+ 'adtrt_popltn_suprt',
+ 'swrg_instlrt',
+ 'rmk',
+ ],
+ fixedColumns: 2,
+ rowStartNum: 3
+};
\ No newline at end of file
diff --git a/apps/st/src/entities/code/@x/posting.ts b/apps/st/src/entities/code/@x/posting.ts
new file mode 100644
index 000000000..0525684d9
--- /dev/null
+++ b/apps/st/src/entities/code/@x/posting.ts
@@ -0,0 +1 @@
+export { CodeSelect } from "../ui/code-select";
diff --git a/apps/st/src/entities/code/api/code-service.ts b/apps/st/src/entities/code/api/code-service.ts
new file mode 100644
index 000000000..0d1055f6a
--- /dev/null
+++ b/apps/st/src/entities/code/api/code-service.ts
@@ -0,0 +1,14 @@
+import { client } from "@/shared/api";
+import { ENDPOINT } from "@/shared/config";
+import type { CodeService, ICode } from "../model/code-interface";
+
+export class CodeApiService implements CodeService {
+ public async getCode(upCd: string): Promise
{
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.CODE}/${upCd}`,
+ );
+ return response.data;
+ }
+}
+
+export const CodeApi = new CodeApiService();
diff --git a/apps/st/src/entities/code/api/lwpocode-service.ts b/apps/st/src/entities/code/api/lwpocode-service.ts
new file mode 100644
index 000000000..818ddb120
--- /dev/null
+++ b/apps/st/src/entities/code/api/lwpocode-service.ts
@@ -0,0 +1,73 @@
+import { client } from "@/shared/api";
+import { ENDPOINT } from "@/shared/config";
+import {
+ LwpoCodeFormSchema,
+ type LwpoCodeService,
+ type LwpoCodeList,
+ type LwpoCodeDetail,
+ type LwpoCodeParams,
+ type LwpoCodeFormData,
+} from "../model/lwpocode-interface";
+
+export class LwpoCodeApiService implements LwpoCodeService {
+ public async getLwpoCodeList(params?: LwpoCodeParams): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.LWPO_CODE}`,
+ { params },
+ );
+ return response.data;
+ }
+
+ public async getUpCodeLwpoCodeList(upCd: string): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/up-cd/${upCd}`,
+ );
+ return response.data;
+ }
+
+ public async lwpoCodeDetail(
+ upCd: string,
+ lwpoCd: string,
+ ): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`,
+ );
+ return response.data;
+ }
+
+ public async createLwpoCode(data: LwpoCodeFormData): Promise {
+ const validatedData = LwpoCodeFormSchema.parse(data);
+ const response = await client.post(
+ `${ENDPOINT.CMS_SERVICE.LWPO_CODE}`,
+ validatedData,
+ );
+ return response.data;
+ }
+
+ public async updateLwpoCode(
+ upCd: string,
+ lwpoCd: string,
+ data: LwpoCodeFormData,
+ ): Promise {
+ const validatedData = LwpoCodeFormSchema.parse(data);
+ const response = await client.put(
+ `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`,
+ validatedData,
+ );
+ return response.data;
+ }
+
+ public async deleteLwpoCode(upCd: string, lwpoCd: string): Promise {
+ await client.delete(
+ `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`,
+ );
+ }
+
+ public async codeRefreshAll(): Promise {
+ await client.post(
+ `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/cache/refresh-all`,
+ );
+ }
+}
+
+export const LwpoCodeApi = new LwpoCodeApiService();
diff --git a/apps/st/src/entities/code/api/upcode-service.ts b/apps/st/src/entities/code/api/upcode-service.ts
new file mode 100644
index 000000000..dcccd9534
--- /dev/null
+++ b/apps/st/src/entities/code/api/upcode-service.ts
@@ -0,0 +1,47 @@
+import { client } from "@/shared/api";
+import { ENDPOINT } from "@/shared/config";
+import {
+ upCodeFormSchema,
+ type UpCodeService,
+ type UpCodeList,
+ type UpCodeDetail,
+ type UpCodeParams,
+ type UpCodeFormData,
+} from "../model/upcode-interface";
+
+export class UpCodeApiService implements UpCodeService {
+ public async getUpCodeList(params?: UpCodeParams): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.UP_CODE}`,
+ { params },
+ );
+ return response.data;
+ }
+
+ public async upCodeDetail(upCd: string): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.UP_CODE}/${upCd}`,
+ );
+ return response.data;
+ }
+
+ public async createUpCode(data: UpCodeFormData): Promise {
+ const validatedData = upCodeFormSchema.parse(data);
+ const response = await client.post(
+ `${ENDPOINT.CMS_SERVICE.UP_CODE}`,
+ validatedData,
+ );
+ return response.data;
+ }
+
+ public async updateUpCode(upCd: string, data: UpCodeFormData): Promise {
+ const validatedData = upCodeFormSchema.parse(data);
+ const response = await client.put(
+ `${ENDPOINT.CMS_SERVICE.UP_CODE}/${upCd}`,
+ validatedData,
+ );
+ return response.data;
+ }
+}
+
+export const UpCodeApi = new UpCodeApiService();
diff --git a/apps/st/src/entities/code/index.ts b/apps/st/src/entities/code/index.ts
new file mode 100644
index 000000000..e166e2c29
--- /dev/null
+++ b/apps/st/src/entities/code/index.ts
@@ -0,0 +1,19 @@
+export { CodeApi } from "./api/code-service";
+export type { ICode } from "./model/code-interface";
+export { CodeSelect } from "./ui/code-select";
+
+export { UpCodeApi } from "./api/upcode-service";
+export {
+ upCodeFormSchema,
+ type UpCode,
+ type UpCodeFormData,
+ type UpCodeParams,
+} from "./model/upcode-interface";
+
+export { LwpoCodeApi } from "./api/lwpocode-service";
+export {
+ LwpoCodeFormSchema,
+ type LwpoCode,
+ type LwpoCodeFormData,
+ type LwpoCodeParams,
+} from "./model/lwpocode-interface";
diff --git a/apps/st/src/entities/code/model/code-interface.ts b/apps/st/src/entities/code/model/code-interface.ts
new file mode 100644
index 000000000..4562e2559
--- /dev/null
+++ b/apps/st/src/entities/code/model/code-interface.ts
@@ -0,0 +1,10 @@
+export interface ICode {
+ lwpoCd: string;
+ upCd: string;
+ lwpoCdNm: string;
+ unit: string;
+}
+
+export interface CodeService {
+ getCode(upCd: string): Promise;
+}
diff --git a/apps/st/src/entities/code/model/lwpocode-interface.ts b/apps/st/src/entities/code/model/lwpocode-interface.ts
new file mode 100644
index 000000000..2538c1c30
--- /dev/null
+++ b/apps/st/src/entities/code/model/lwpocode-interface.ts
@@ -0,0 +1,69 @@
+import { z } from "zod";
+import type { PageableParams, PageableList } from "@/shared/model";
+
+export interface LwpoCodeParams extends PageableParams {
+ upCd?: string;
+ lwpoCd?: string;
+ lwpoCdNm?: string;
+ useYn?: string;
+}
+
+export interface LwpoCode {
+ lwpoCd: string;
+ upCd: string;
+ lwpoCdNm: string;
+ lwpoCdExpln: string;
+ unit: string;
+ prntLwpoCd: string;
+ useYn: string;
+ sortSeq: number;
+ regDt: string;
+ rgTr: string;
+ rgTrNm: string;
+ mdfcnDt: string;
+ mdFr: string;
+}
+
+export interface LwpoCodeDetail extends LwpoCodeFormData {
+ lwpoCd: string;
+ upCd: string;
+ lwpoCdNm: string;
+ lwpoCdExpln: string;
+ unit: string;
+ prntLwpoCd: string;
+ useYn: string;
+ sortSeq: 0;
+}
+
+export const LwpoCodeFormSchema = z.object({
+ lwpoCd: z.string().max(10).nonempty(),
+ upCd: z.string().max(10).nonempty(),
+ lwpoCdNm: z.string().max(100).nonempty(),
+ lwpoCdExpln: z.string().max(1000).nullish(),
+ unit: z.string().max(20).nullish(),
+ prntLwpoCd: z.string().max(10).nullish(),
+ useYn: z.string(),
+ sortSeq: z.number(),
+ delYn: z.string(),
+});
+
+export type LwpoCodeFormData = z.infer;
+export type LwpoCodeList = PageableList;
+
+export interface LwpoCodeService {
+ getLwpoCodeList(params: LwpoCodeParams): Promise;
+ getUpCodeLwpoCodeList(upCd: string): Promise;
+ lwpoCodeDetail(upCd: string, lwpoCd: string): Promise;
+ updateLwpoCode(
+ upCd: string,
+ lwpoCd: string,
+ data: LwpoCodeFormData,
+ ): Promise;
+ createLwpoCode(data: LwpoCodeFormData): Promise;
+ deleteLwpoCode(
+ upCd: string,
+ lwpoCd: string,
+ data: LwpoCodeFormData,
+ ): Promise;
+ codeRefreshAll(): Promise;
+}
diff --git a/apps/st/src/entities/code/model/upcode-interface.ts b/apps/st/src/entities/code/model/upcode-interface.ts
new file mode 100644
index 000000000..73bf4f3ae
--- /dev/null
+++ b/apps/st/src/entities/code/model/upcode-interface.ts
@@ -0,0 +1,52 @@
+import { z } from "zod";
+import type { PageableParams, PageableList } from "@/shared/model";
+
+export interface UpCodeParams extends PageableParams {
+ keyword?: string;
+ upCd?: string;
+ cdSe?: string;
+ useYn?: string;
+ siteId?: string;
+}
+
+export interface UpCode {
+ upCd: string;
+ upCdNm: string;
+ upCdExpln: string;
+ cdSe: string;
+ siteId: string;
+ useYn: string;
+ regDt: string;
+}
+
+export interface UpCodeDetail extends UpCodeFormData {
+ upCd: string;
+ upCdNm: string;
+ upCdExpln: string;
+ cdSe: string;
+ siteId: string;
+ useYn: string;
+ delYn: string;
+ regDt: string;
+ rgTrNm: string;
+}
+
+export const upCodeFormSchema = z.object({
+ upCd: z.string().max(10).nonempty(),
+ upCdNm: z.string().max(100).nonempty(),
+ upCdExpln: z.string().max(1000).nullish(),
+ cdSe: z.string().max(10).nonempty(),
+ siteId: z.string().max(2).nullish(),
+ useYn: z.string(),
+ delYn: z.string(),
+});
+
+export type UpCodeFormData = z.infer;
+export type UpCodeList = PageableList;
+
+export interface UpCodeService {
+ getUpCodeList(params: UpCodeParams): Promise;
+ upCodeDetail(upCodeId: string): Promise;
+ updateUpCode(upCodeId: string, data: UpCodeFormData): Promise;
+ createUpCode(data: UpCodeFormData): Promise;
+}
diff --git a/apps/st/src/entities/code/ui/code-select.tsx b/apps/st/src/entities/code/ui/code-select.tsx
new file mode 100644
index 000000000..08df4ce51
--- /dev/null
+++ b/apps/st/src/entities/code/ui/code-select.tsx
@@ -0,0 +1,47 @@
+import { useQuery } from "@tanstack/react-query";
+import {
+ cn,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@common/components";
+import { ENDPOINT } from "@/shared/config";
+import { CodeApi } from "../api/code-service";
+
+type CodeSelectProps = {
+ upCd: string;
+ placeholder?: string;
+ className?: string;
+} & React.ComponentPropsWithoutRef;
+
+function CodeSelect({
+ upCd,
+ placeholder = "선택",
+ className,
+ ...props
+}: CodeSelectProps) {
+ const { data: code } = useQuery({
+ queryKey: [ENDPOINT.CMS_SERVICE.CODE, upCd],
+ queryFn: () => CodeApi.getCode(upCd),
+ enabled: !!upCd,
+ });
+
+ return (
+
+ );
+}
+
+export { CodeSelect };
diff --git a/apps/st/src/pages/statistics/ui/statistics-page.tsx b/apps/st/src/pages/statistics/ui/statistics-page.tsx
index 3efa3b04f..7513b0ffe 100644
--- a/apps/st/src/pages/statistics/ui/statistics-page.tsx
+++ b/apps/st/src/pages/statistics/ui/statistics-page.tsx
@@ -6,6 +6,8 @@ import { useTabStore } from "@/store/tab-store";
import Dashboard from "@/components/Dashboard";
import { SkeletonPage } from "@/shared/ui/skeleton-page";
+import "@rsone/components/index.css";
+
const StatisticsPage = () => {
const { tabs, activeTabId } = useTabStore();
@@ -38,7 +40,7 @@ const StatisticsPage = () => {
{tabs.map((tab) => {
// 렌더링 시점에 컴포넌트가 없으면 바로 로드
if (!componentCache.current.has(tab.id)) {
- const componentName = tab.id;
+ const componentName = tab.urlPath;
const parentComponentName = tab.parentEngNm;
const DynamicComponent = dynamic(
diff --git a/apps/st/src/shared/config/api.ts b/apps/st/src/shared/config/api.ts
index 61c2bec0e..8325c0e32 100644
--- a/apps/st/src/shared/config/api.ts
+++ b/apps/st/src/shared/config/api.ts
@@ -65,7 +65,7 @@ export const ENDPOINT = {
POST_EVALUATION: "/board-service/api/v1/pstEvl",
},
ST_SERVICE: {
- COMMON: "/st-service/api/v1/common",
+ COMMON: "/st-management-service/api/v1/common",
}
};
diff --git a/apps/st/src/store/tab-store.ts b/apps/st/src/store/tab-store.ts
index f5b49bb29..cdbaad91f 100644
--- a/apps/st/src/store/tab-store.ts
+++ b/apps/st/src/store/tab-store.ts
@@ -4,7 +4,8 @@ import type { MenuItem } from "@/entities/menu";
export interface Tab {
id: string; // 고유 ID (menuEngNm)
label: string; // 탭에 표시될 이름
- path: string; // URL 경로
+ path: string; // 메뉴 경로
+ urlPath: string; // url 경로
parentEngNm: string; // 부모 menuEngNm
menuItem: MenuItem; // 메뉴 정보
}
@@ -38,11 +39,12 @@ const useTabStoreHook = create((set, get) => ({
return true;
}
- if (tabs.length < 10) {
+ if (tabs.length < 100) {
const newTab: Tab = {
id: tabId,
label: menuItem.menuKornNm,
path: menuItem.menuPath,
+ urlPath: menuItem.urlPath,
parentEngNm: menuList?.find((m) => m.menuCd === menuItem.parentId)?.menuEngNm || '',
menuItem,
};
@@ -112,5 +114,4 @@ const useTabStoreHook = create((set, get) => ({
}));
export const useTabStore = useTabStoreHook;
-export const getTabStoreState = () => useTabStoreHook.getState();
-
+export const getTabStoreState = () => useTabStoreHook.getState();
\ No newline at end of file
diff --git a/apps/st/tsconfig.json b/apps/st/tsconfig.json
index 5b8c29899..e91347649 100644
--- a/apps/st/tsconfig.json
+++ b/apps/st/tsconfig.json
@@ -27,7 +27,8 @@
"@/*": ["./*"],
"@common/components": ["../../../packages/common/components/src/"],
"@common/config": ["../../../packages/common/config/dist/"],
- "@common/network": ["../../../packages/common/network/"]
+ "@common/network": ["../../../packages/common/network/"],
+ "@rsone/components": ["../../../packages/rsone/dist/"]
}
},
"include": [
diff --git a/packages/rsone/.gitignore b/packages/rsone/.gitignore
new file mode 100644
index 000000000..3ece3c8a7
--- /dev/null
+++ b/packages/rsone/.gitignore
@@ -0,0 +1,26 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+.env*
\ No newline at end of file
diff --git a/packages/rsone/components/grid/DataGrid.tsx b/packages/rsone/components/grid/DataGrid.tsx
new file mode 100644
index 000000000..4c9a50522
--- /dev/null
+++ b/packages/rsone/components/grid/DataGrid.tsx
@@ -0,0 +1,352 @@
+import { useRef, useEffect } from "react"
+import { ValueType } from "realgrid"
+
+import RealGrid, { GridFitStyle, type DisplayOptions, SortDirection } from "realgrid"
+import { RealGridReact, RGDataColumn } from "realgrid-react"
+import type { DataGridProps } from "../type/DataGrid.type"
+import JSZip from "jszip"
+import * as XLSX from "xlsx"
+import { Button } from "@common/components";
+import { useAlertStore } from "../store/alert-store";
+import { useTabStore } from "../store/tab-store";
+
+// RealGrid 타입 확장
+declare module "realgrid" {
+ interface GridView {
+ setOptions(options: {
+ footer?: { visible?: boolean };
+ summaryMode?: 'aggregate' | 'statistical' | 'none';
+ checkBar?: { visible?: boolean };
+ }): void;
+ }
+}
+
+// Window 인터페이스 확장
+declare global {
+ interface Window {
+ JSZip: typeof JSZip;
+ }
+}
+
+// RealGrid가 jszip을 사용할 수 있도록 전역에 등록
+if (typeof window !== "undefined") {
+ window.JSZip = JSZip;
+}
+
+RealGrid.setLicenseKey(process.env.NEXT_PUBLIC_ST_REALGRID_KEY || '');
+
+const DataGrid = ({ columnInfo, rowStartNum, isExcelImport = true, isExcelExport = true, fixedColumns, showHeaderCheckbox = false, rowData, setRowData }: DataGridProps) => {
+ const { columns, columnLayout } = columnInfo;
+ const gridRef = useRef(null);
+ const fileInputRef = useRef(null);
+ const { setMessage: alert } = useAlertStore((state) => state);
+ const activeTabId = useTabStore((state) => state.activeTabId);
+ const menuList = useTabStore((state) => state.menuList);
+
+ const exportExcel = () => {
+ if (rowData.length === 0) {
+ alert('데이터가 없습니다.');
+ return;
+ }
+
+ if (!gridRef.current?.gridView) return;
+
+ const gridView = gridRef.current.gridView;
+
+ const menuName = menuList
+ ?.flatMap((m) => m.children || [])
+ .flatMap((subM) => subM || [])
+ .find((s) => s.menuEngNm === activeTabId)
+ ?.menuKornNm || 'export';
+
+ gridView.exportGrid({
+ type: 'excel',
+ target: 'local',
+ fileName: `${menuName}.xlsx`,
+ footer: 'hidden',
+ done: () => {
+ alert('내보내기가 완료되었습니다.');
+ },
+ });
+ };
+
+ const importExcel = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleFileChange = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ try {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ try {
+ const data = event.target?.result;
+ if (!data) {
+ alert('파일을 읽을 수 없습니다.');
+ return;
+ }
+
+ // SheetJS로 엑셀 파일을 JSON으로 변환
+ const workbook = XLSX.read(data, { type: 'array', cellDates: true });
+ const firstSheetName = workbook.SheetNames[0];
+ const worksheet = workbook.Sheets[firstSheetName || ''];
+
+ if (!worksheet || !worksheet['!ref']) {
+ alert('워크시트를 읽을 수 없습니다.');
+ return;
+ }
+
+ const range = XLSX.utils.decode_range(worksheet['!ref']);
+ const headerRowCount = rowStartNum || 1; // 기본값 1
+ const headerStartRow = Math.max(0, headerRowCount - 1); // 0-based index
+
+ // 각 컬럼별로 헤더 찾기 (rowStartNum 행부터 위로 올라가면서)
+ const headers: string[] = [];
+ const headerRowIndexes: number[] = [];
+
+ for (let col = range.s.c; col <= range.e.c; col++) {
+ let headerValue = '';
+ let headerRowIndex = headerStartRow;
+
+ // rowStartNum 행부터 시작해서 위로 올라가면서 헤더 찾기
+ for (let rowIndex = headerStartRow; rowIndex >= 0; rowIndex--) {
+ const cell = worksheet[XLSX.utils.encode_cell({ r: rowIndex, c: col })];
+ if (cell && cell.v !== undefined && cell.v !== null && cell.v !== '') {
+ headerValue = String(cell.v);
+ headerRowIndex = rowIndex;
+ break;
+ }
+ }
+
+ // 헤더를 찾지 못한 경우 기본값 사용
+ if (!headerValue) {
+ headerValue = `Column${col + 1}`;
+ headerRowIndex = headerStartRow;
+ }
+
+ headers.push(headerValue);
+ headerRowIndexes.push(headerRowIndex);
+ }
+
+ // 실제 데이터가 시작하는 행 찾기 (헤더가 있는 최대 행 번호 + 1)
+ const maxHeaderRowIndex = headerRowIndexes.length > 0 ? Math.max(...headerRowIndexes) : headerStartRow;
+ const dataStartRow = maxHeaderRowIndex + 1;
+
+ // 데이터 행 읽기
+ const dataRows: Record[] = [];
+ for (let row = dataStartRow; row <= range.e.r; row++) {
+ const rowData: Record = {};
+ let hasData = false;
+
+ for (let colIndex = 0; colIndex < headers.length; colIndex++) {
+ const col = range.s.c + colIndex;
+ const cell = worksheet[XLSX.utils.encode_cell({ r: row, c: col })];
+ const headerName = headers[colIndex] || `Column${colIndex + 1}`;
+
+ let cellValue: string | number = '';
+ if (cell && cell.v !== undefined && cell.v !== null) {
+ // 엑셀 셀 타입이 숫자이거나 값이 숫자인 경우 숫자로 변환
+ if (cell.t === 'n' || (typeof cell.v === 'number' && !isNaN(cell.v))) {
+ cellValue = cell.v as number;
+ } else {
+ const stringValue = String(cell.v);
+ // 문자열이지만 숫자로 변환 가능한 경우 숫자로 변환
+ const numericValue = Number(stringValue.replace(/,/g, ''));
+ if (!isNaN(numericValue) && stringValue.trim() !== '') {
+ cellValue = numericValue;
+ } else {
+ cellValue = stringValue;
+ }
+ }
+ }
+
+ if (cellValue !== '' && cellValue !== null && cellValue !== undefined) {
+ hasData = true;
+ }
+
+ rowData[headerName] = cellValue;
+ }
+
+ // 데이터가 하나라도 있는 행만 추가
+ if (hasData) {
+ dataRows.push(rowData);
+ }
+ }
+ setRowData(dataRows);
+ } catch (error) {
+ console.error('엑셀 파일 파싱 오류:', error);
+ alert('엑셀 파일 파싱 중 오류가 발생했습니다.');
+ } finally {
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ }
+ };
+
+ reader.onerror = () => {
+ alert('파일 읽기 중 오류가 발생했습니다.');
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ reader.readAsArrayBuffer(file);
+ } catch (error) {
+ console.error('엑셀 파일 가져오기 오류:', error);
+ alert('엑셀 파일 가져오기 중 오류가 발생했습니다.');
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ }
+ };
+
+ useEffect(() => {
+ if (!gridRef.current?.gridView || !gridRef.current?.dataProvider) return;
+
+ const gridView = gridRef.current.gridView;
+ const dataProvider = gridRef.current.dataProvider;
+
+ const fieldsToSet: Array<{ fieldName: string; dataType: string }> = [];
+ const fieldMap = new Map();
+
+ columns.forEach(col => {
+ if (col.valueType && col.fieldName && !fieldMap.has(col.fieldName)) {
+ fieldMap.set(col.fieldName, col.valueType);
+ fieldsToSet.push({
+ fieldName: col.fieldName,
+ dataType: col.valueType
+ });
+ }
+ });
+
+ // 필드가 이미 존재하는지 확인하고, valueType이 설정된 필드만 업데이트
+ if (fieldsToSet.length > 0) {
+ try {
+ // 기존 필드 정보 가져오기
+ const existingFields = (dataProvider as { getFields?: () => Array<{ fieldName: string; dataType?: string }> }).getFields?.() || [];
+ const existingFieldMap = new Map(existingFields.map(f => [f.fieldName, f.dataType || 'text']));
+
+ // valueType이 설정된 필드 중 dataType이 다른 필드만 업데이트
+ const fieldsToUpdate = fieldsToSet.filter(field => {
+ const existingDataType = existingFieldMap.get(field.fieldName);
+ return existingDataType !== field.dataType;
+ });
+
+ if (fieldsToUpdate.length > 0) {
+ // 기존 필드 정보를 유지하면서 dataType만 업데이트
+ // 컬럼 정의에 valueType이 있으면 그것을 우선 사용, 없으면 기존 필드의 dataType 사용
+ const allFields = existingFields.map(f => ({
+ fieldName: f.fieldName,
+ dataType: fieldMap.get(f.fieldName) || f.dataType || 'text'
+ }));
+
+ // 새로 추가할 필드가 있으면 추가
+ fieldsToSet.forEach(field => {
+ if (!existingFieldMap.has(field.fieldName)) {
+ allFields.push({ fieldName: field.fieldName, dataType: field.dataType });
+ }
+ });
+
+ // setFields를 호출하여 필드의 dataType 업데이트
+ (dataProvider as { setFields: (fields: Array<{ fieldName: string; dataType: string }>) => void })
+ .setFields(allFields);
+ }
+ } catch (error) {
+ console.warn('Failed to set field dataType:', error);
+ }
+ }
+
+ (gridView.displayOptions as DisplayOptions).fitStyle = GridFitStyle.EVEN;
+ gridView.header.height = 60;
+
+ // 헤더 체크박스 표시 여부 설정
+ gridView.setOptions({
+ checkBar: { visible: showHeaderCheckbox },
+ });
+
+ // footer가 있는 컬럼이 있으면 footer 높이 설정
+ const hasFooter = columns.some(col => col.footer);
+ if (hasFooter) {
+ gridView.footer.height = 40;
+ }
+
+ // 고정 컬럼 설정
+ if (fixedColumns !== undefined && fixedColumns > 0) {
+ gridView.setFixedOptions({
+ colCount: fixedColumns,
+ });
+ }
+
+ gridView.setOptions({
+ summaryMode: "aggregate"
+ });
+
+ // footer의 valueCallback 설정
+ columns.forEach(col => {
+ if (col.footer && col.footer.valueCallback) {
+ const column = gridView.columnByName(col.name);
+ if (column && column.footer) {
+ (column.footer as { valueCallback?: typeof col.footer.valueCallback }).valueCallback = col.footer.valueCallback;
+ }
+ }
+ });
+
+ }, [columns, columnLayout, fixedColumns, rowData, showHeaderCheckbox]);
+
+ return (
+
+
+
+ {isExcelImport &&
+
+ }
+ {isExcelExport &&
+
+ }
+
+
+
+ {columns.map((column) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default DataGrid;
\ No newline at end of file
diff --git a/packages/rsone/components/grid/TabDataGrid.tsx b/packages/rsone/components/grid/TabDataGrid.tsx
new file mode 100644
index 000000000..191ebaea3
--- /dev/null
+++ b/packages/rsone/components/grid/TabDataGrid.tsx
@@ -0,0 +1,470 @@
+import { useRef, useEffect, useState } from "react"
+import { ValueType } from "realgrid"
+
+import RealGrid, { GridFitStyle, type DisplayOptions } from "realgrid"
+import { RealGridReact, RGDataColumn } from "realgrid-react"
+import type { TabDataGridProps } from "../type/DataGrid.type"
+import JSZip from "jszip"
+import * as XLSX from "xlsx"
+import { useAlertStore } from "../store/alert-store";
+import { useTabStore } from "../store/tab-store";
+
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@common/components";
+import { Button } from "@common/components";
+
+// RealGrid 타입 확장
+declare module "realgrid" {
+ interface GridView {
+ setOptions(options: {
+ footer?: { visible?: boolean };
+ summaryMode?: 'aggregate' | 'statistical' | 'none';
+ checkBar?: { visible?: boolean };
+ }): void;
+ }
+}
+
+// Window 인터페이스 확장
+declare global {
+ interface Window {
+ JSZip: typeof JSZip;
+ }
+}
+
+// RealGrid가 jszip을 사용할 수 있도록 전역에 등록
+if (typeof window !== "undefined") {
+ window.JSZip = JSZip;
+}
+
+RealGrid.setLicenseKey(process.env.NEXT_PUBLIC_ST_REALGRID_KEY || '');
+
+const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, rows = [], showHeaderCheckbox = false, rowDataMap, setRowDataMap }: TabDataGridProps) => {
+ // 각 탭별로 그리드 ref와 데이터를 관리
+ const gridRefs = useRef