diff --git a/apps/cm/.env.local b/apps/cm/.env.local index a544bdf7e..1ef6a3045 100644 --- a/apps/cm/.env.local +++ b/apps/cm/.env.local @@ -5,4 +5,6 @@ NEXT_PUBLIC_SITE_ID=CS NEXT_PUBLIC_SUB_DOMAIN=http://localhost:13000 NEXT_PUBLIC_SERVER_API_URL=http://dev.onasys.co.kr:8000 NEXT_PUBLIC_LINK_BACKEND_API_HOST=http://rsone.iptime.org:10014 -NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2 \ No newline at end of file +NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2 + +NEXT_PUBLIC_SERVER_API_URL2=http://con.mind-one.co.kr:11083/ \ No newline at end of file diff --git a/apps/cm/.gitignore b/apps/cm/.gitignore index b63718b61..c3326d730 100644 --- a/apps/cm/.gitignore +++ b/apps/cm/.gitignore @@ -10,6 +10,8 @@ !.yarn/releases !.yarn/versions +pnpm_lock.yaml + # testing /coverage diff --git a/apps/cm/middleware.ts b/apps/cm/middleware.ts index e0316fa9f..ac1e5464b 100644 --- a/apps/cm/middleware.ts +++ b/apps/cm/middleware.ts @@ -11,6 +11,18 @@ import { const serverApiUrl = process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000"; + const serverApiUrl2 = + process.env.NEXT_PUBLIC_SERVER_API_URL2 ?? serverApiUrl; + + const serverApiUrl3 = + process.env.NEXT_PUBLIC_SERVER_API_URL3 ?? serverApiUrl; + + const serverApiUrl4 = + process.env.NEXT_PUBLIC_SERVER_API_URL4 ?? serverApiUrl; + + const serverApiUrl5 = + process.env.NEXT_PUBLIC_SERVER_API_URL5 ?? serverApiUrl; + export function middleware(req: NextRequest) { const pathname = req.nextUrl.pathname; const searchParams = req.nextUrl.searchParams; @@ -52,6 +64,97 @@ export function middleware(req: NextRequest) { }); } + + if (pathname.startsWith("/sewerage-service")) { + const requestHeaders = new Headers(req.headers); + const token = req.cookies.get(CLAIM_NAME)?.value; + const userId = req.cookies.get(AUTH_USER_ID)?.value; + const newUrl = `${serverApiUrl2}${pathname}`; + + if (token) requestHeaders.set(CLAIM_NAME, token); + if (userId) requestHeaders.set(AUTH_USER_ID, userId); + + logMessages.push(` Rewritten URL: ${newUrl}`); + logMessages.push("\n--------------------------------\n"); + console.log(logMessages.join("\n")); + + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } + + if (pathname.startsWith("/reuse-service")) { + const requestHeaders = new Headers(req.headers); + const token = req.cookies.get(CLAIM_NAME)?.value; + const userId = req.cookies.get(AUTH_USER_ID)?.value; + const newUrl = `${serverApiUrl3}${pathname}`; + + if (token) requestHeaders.set(CLAIM_NAME, token); + if (userId) requestHeaders.set(AUTH_USER_ID, userId); + + logMessages.push(` Rewritten URL: ${newUrl}`); + logMessages.push("\n--------------------------------\n"); + console.log(logMessages.join("\n")); + + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } + + if (pathname.startsWith("/operation-service")) { + const requestHeaders = new Headers(req.headers); + const token = req.cookies.get(CLAIM_NAME)?.value; + const userId = req.cookies.get(AUTH_USER_ID)?.value; + const newUrl = `${serverApiUrl4}${pathname}`; + + if (token) requestHeaders.set(CLAIM_NAME, token); + if (userId) requestHeaders.set(AUTH_USER_ID, userId); + + logMessages.push(` Rewritten URL: ${newUrl}`); + logMessages.push("\n--------------------------------\n"); + console.log(logMessages.join("\n")); + + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } + + if (pathname.startsWith("/ic-service")) { + const requestHeaders = new Headers(req.headers); + const token = req.cookies.get(CLAIM_NAME)?.value; + const userId = req.cookies.get(AUTH_USER_ID)?.value; + const newUrl = `${serverApiUrl5}${pathname}`; + + if (token) requestHeaders.set(CLAIM_NAME, token); + if (userId) requestHeaders.set(AUTH_USER_ID, userId); + + logMessages.push(` Rewritten URL: ${newUrl}`); + logMessages.push("\n--------------------------------\n"); + console.log(logMessages.join("\n")); + + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } + + if (pathname.startsWith("/dc-service")) { + const requestHeaders = new Headers(req.headers); + const token = req.cookies.get(CLAIM_NAME)?.value; + const userId = req.cookies.get(AUTH_USER_ID)?.value; + const newUrl = `${serverApiUrl5}${pathname}`; + + if (token) requestHeaders.set(CLAIM_NAME, token); + if (userId) requestHeaders.set(AUTH_USER_ID, userId); + + logMessages.push(` Rewritten URL: ${newUrl}`); + logMessages.push("\n--------------------------------\n"); + console.log(logMessages.join("\n")); + + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } + if (pathname.startsWith("/user-service")) { const queryParams = searchParams.toString(); const newUrl = `${serverApiUrl}${pathname}${ diff --git a/apps/cm/public/images/region_map.svg b/apps/cm/public/images/region_map.svg new file mode 100644 index 000000000..f327bb9fc --- /dev/null +++ b/apps/cm/public/images/region_map.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/cm/public/images/region_map_bg.png b/apps/cm/public/images/region_map_bg.png new file mode 100644 index 000000000..6f59f58cc Binary files /dev/null and b/apps/cm/public/images/region_map_bg.png differ diff --git a/apps/cm/src/entities/auth/lib/token-manager.ts b/apps/cm/src/entities/auth/lib/token-manager.ts index 0e761486a..21ed00218 100644 --- a/apps/cm/src/entities/auth/lib/token-manager.ts +++ b/apps/cm/src/entities/auth/lib/token-manager.ts @@ -19,14 +19,37 @@ export class TokenManager { } public setToken(key: string, value: string): void { + // 환경에 따른 도메인 설정 + const domain = this.getCookieDomain(); + setCookie(key, value, { secure: window.location.protocol === "https:", sameSite: "lax", + domain: domain, + path: "/", // 모든 경로에서 접근 가능 }); } + private getCookieDomain(): string | undefined { + const hostname = window.location.hostname; + + // 개발 환경에서는 localhost 도메인 사용 + if (hostname === "localhost" || hostname === "127.0.0.1") { + return undefined; // localhost에서는 도메인 설정하지 않음 + } + + // 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용 + if (hostname.includes("hasuon.or.kr")) { + return ".hasuon.or.kr"; + } + + // 기타 환경에서는 현재 호스트명 사용 + return hostname; + } + public removeToken(key: string): void { - removeCookie(key); + const domain = this.getCookieDomain(); + removeCookie(key, { domain, path: "/" }); } public setupRefreshTimer( diff --git a/apps/cm/src/entities/board/@x/post.ts b/apps/cm/src/entities/board/@x/post.ts new file mode 100644 index 000000000..9d2b29076 --- /dev/null +++ b/apps/cm/src/entities/board/@x/post.ts @@ -0,0 +1 @@ +export { BoardApi } from "../api/board-service"; diff --git a/apps/cm/src/entities/board/api/board-service.ts b/apps/cm/src/entities/board/api/board-service.ts new file mode 100644 index 000000000..dc474b3ad --- /dev/null +++ b/apps/cm/src/entities/board/api/board-service.ts @@ -0,0 +1,60 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + Board, + BoardFormData, + BoardList, + BoardParams, + BoardPopupList, + BoardPopupParams, + BoardService, +} from "../model/board-interface"; + +export class BoardApiService implements BoardService { + public async getBoardList(params?: BoardParams): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.BBS}`, + { params }, + ); + return response.data; + } + + public async getBoardPopupList( + params: BoardPopupParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.BBS}`, + { params }, + ); + return response.data; + } + + public async getBoardInfo(bbsId: string): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`, + ); + return response.data; + } + + public async createBoard(data: BoardFormData): Promise { + const response = await client.post( + `${ENDPOINT.BOARD_SERVICE.BBS}`, + data, + ); + return response.data; + } + + public async updateBoard(bbsId: string, data: BoardFormData): Promise { + const response = await client.put( + `${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`, + data, + ); + return response.data; + } + + public async deleteBoard(bbsId: string): Promise { + await client.delete(`${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`); + } +} + +export const BoardApi = new BoardApiService(); diff --git a/apps/cm/src/entities/board/api/board-template-service.ts b/apps/cm/src/entities/board/api/board-template-service.ts new file mode 100644 index 000000000..be3efde75 --- /dev/null +++ b/apps/cm/src/entities/board/api/board-template-service.ts @@ -0,0 +1,17 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + BoardTemplate, + BoardTemplateService, +} from "../model/board-template-interface"; + +export class BoardTemplateApiService implements BoardTemplateService { + public async getBoardTemplate(bbsTypeCd: string): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.BBS_TEMPLATES}/${bbsTypeCd}`, + ); + return response.data; + } +} + +export const BoardTemplateApi = new BoardTemplateApiService(); diff --git a/apps/cm/src/entities/board/index.ts b/apps/cm/src/entities/board/index.ts new file mode 100644 index 000000000..ddd10f382 --- /dev/null +++ b/apps/cm/src/entities/board/index.ts @@ -0,0 +1,11 @@ +export { BoardApi } from "./api/board-service"; +export { + boardFormSchema, + BOARD_POSTING_CODE, + type Board, + type BoardFormData, + type BoardParams, + type BoardPopup, + type BoardPostingCode, +} from "./model/board-interface"; +export { BoardTemplateApi } from "./api/board-template-service"; diff --git a/apps/cm/src/entities/board/model/board-interface.ts b/apps/cm/src/entities/board/model/board-interface.ts new file mode 100644 index 000000000..8b7859223 --- /dev/null +++ b/apps/cm/src/entities/board/model/board-interface.ts @@ -0,0 +1,99 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; +import type { BbsType } from "@/shared/store"; + +export const BOARD_POSTING_CODE = { + 팝업게시: "01", + 상단게시: "02", + 기간게시: "03", +} as const; + +export type BoardPostingCode = + (typeof BOARD_POSTING_CODE)[keyof typeof BOARD_POSTING_CODE]; + +export interface BoardParams extends PageableParams { + keywordType?: string; + keyword?: string; + bbsId?: number; + bbsTypeCd?: string; + bbsNm?: string; + bbsExpln?: string; + rfrncUseYn?: string; + atflUseYn?: string; + atflMaxCnt?: number; + atflMaxFileSz?: number; + ansUseYn?: string; + cmntUseYn?: string; + dgstfnEvlUseYn?: string; + smsUseYn?: string; + upndFixUseYn?: string; + pstgPrdUseYn?: string; + popupUseYn?: string; + delYn?: string; +} + +export interface BoardPopupParams extends PageableParams { + bbsNm?: string; +} + +export interface Board { + bbsId: number; + bbsTypeCd: BbsType; + bbsTypeNm: string; + bbsNm: string; + bbsExpln: string; + rfrncUseYn: string; + atflUseYn: string; + atflMaxCnt: number; + atflMaxFileSz: number; + ansUseYn: string; + ansUseAuthrt: string; + cmntUseYn: string; + dgstfnEvlUseYn: string; + smsUseYn: string; + upndFixUseYn: string; + pstgPrdUseYn: string; + popupUseYn: string; + delYn: string; + useYn: string; + prvcIndctYn: string; + mdfcnDt: string; + mdfrNm: string; +} + +export interface BoardPopup extends Board { + pstCnt: number; +} + +export const boardFormSchema = z.object({ + bbsTypeCd: z.string(), + bbsNm: z.string(), + bbsExpln: z.string(), + rfrncUseYn: z.string(), + atflUseYn: z.string(), + atflMaxCnt: z.number().nullish(), + atflMaxFileSz: z.number().nullish(), + ansUseYn: z.string(), + ansUseAuthrt: z.string().nullish(), + cmntUseYn: z.string(), + dgstfnEvlUseYn: z.string(), + smsUseYn: z.string(), + upndFixUseYn: z.string(), + pstgPrdUseYn: z.string(), + popupUseYn: z.string(), + useYn: z.string(), + prvcIndctYn: z.string(), +}); + +export type BoardList = PageableList; +export type BoardPopupList = PageableList; +export type BoardFormData = z.infer; + +export interface BoardService { + getBoardList: (params: BoardParams) => Promise; + getBoardPopupList: (params: BoardPopupParams) => Promise; + getBoardInfo: (bbsId: string) => Promise; + createBoard: (data: BoardFormData) => Promise; + updateBoard: (bbsId: string, data: BoardFormData) => Promise; + deleteBoard: (bbsId: string) => Promise; +} diff --git a/apps/cm/src/entities/board/model/board-template-interface.ts b/apps/cm/src/entities/board/model/board-template-interface.ts new file mode 100644 index 000000000..6a7f84fef --- /dev/null +++ b/apps/cm/src/entities/board/model/board-template-interface.ts @@ -0,0 +1,24 @@ +export interface BoardTemplate { + bbsTypeCd: string; + rfrncUseYn: string; + atflUseYn: string; + atflMaxCnt: number; + atflMaxFileSz: number; + ansUseYn: string; + cmntUseYn: string; + dgstfnEvlUseYn: string; + smsUseYn: string; + upndFixUseYn: string; + pstgPrdUseYn: string; + popupUseYn: string; + regDt: string; + rgtr: string; + mdfcnDt: string; + mdfr: string; + rgtrNm: string; + mdfrNm: string; +} + +export interface BoardTemplateService { + getBoardTemplate: (bbsTypeCd: string) => Promise; +} diff --git a/apps/cm/src/entities/post/api/post-service.ts b/apps/cm/src/entities/post/api/post-service.ts new file mode 100644 index 000000000..b7016b780 --- /dev/null +++ b/apps/cm/src/entities/post/api/post-service.ts @@ -0,0 +1,97 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import { + postFormSchema, + postUpdateFormSchema, + type Post, + type MovePost, + type DeletePost, + type PostInfo, + type PostList, + type MyPostList, + type PostParams, + type MyPostParams, + type PostFormData, + type PostUpdateFormData, + type PostService, +} from "../model/post-interface"; + +export class PostApiService implements PostService { + public async getPostList( + bbsId: number, + params: PostParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}`, + { params }, + ); + return response.data; + } + + public async getMyPostList( + params?: MyPostParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.POSTS}/mypost`, + { params }, + ); + return response.data; + } + + public async getPostInfo(bbsId: number, pstNo: number): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`, + ); + return response.data; + } + + public async createPost(data: PostFormData): Promise { + const validatedData = postFormSchema.parse(data); + const response = await client.post( + `${ENDPOINT.BOARD_SERVICE.POSTS}`, + validatedData, + ); + return response.data; + } + + public async updatePost( + bbsId: number, + pstNo: number, + data: PostUpdateFormData, + ): Promise { + const validatedData = postUpdateFormSchema.parse(data); + const response = await client.put( + `${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`, + validatedData, + ); + return response.data; + } + + public async deletePost( + bbsId: number, + pstNo: number, + delRsn?: string, + ): Promise { + await client.delete(`${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`, { + params: { delRsn }, + }); + } + + public async softDeletePost(data: DeletePost[]): Promise { + await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/softDeleteSels`, data); + } + + public async hardDeletePost(data: DeletePost[]): Promise { + await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/hardDeleteSels`, data); + } + + public async movePost(data: MovePost[]): Promise { + await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/movePstByBbsId`, data); + } + + public async restorePost(data: DeletePost[]): Promise { + await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/restoreSels`, data); + } +} + +export const PostApi = new PostApiService(); diff --git a/apps/cm/src/entities/post/index.ts b/apps/cm/src/entities/post/index.ts new file mode 100644 index 000000000..e0241e2f4 --- /dev/null +++ b/apps/cm/src/entities/post/index.ts @@ -0,0 +1,17 @@ +export { PostApi } from "./api/post-service"; +export { + postFormSchema, + postUpdateFormSchema, + type Post, + type MyPost, + type MovePost, + type DeletePost, + type PostList, + type PostParams, + type MyPostParams, + type PostFormData, + type PostUpdateFormData, + type PostService, +} from "./model/post-interface"; +export { deleteReasonMessages } from "./model/delete-reason-messages"; +export { PostPageGuard } from "./lib/post-page-guard"; diff --git a/apps/cm/src/entities/post/lib/post-page-guard.tsx b/apps/cm/src/entities/post/lib/post-page-guard.tsx new file mode 100644 index 000000000..46875f1fc --- /dev/null +++ b/apps/cm/src/entities/post/lib/post-page-guard.tsx @@ -0,0 +1,39 @@ +import { useEffect } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { usePathname, useRouter } from "next/navigation"; +import { BoardApi } from "@/entities/board/@x/post"; +import { useAlertStore, useMenuStore } from "@/shared/lib"; + +function PostPageGuard({ + bbsId, + children, +}: { + bbsId: string; + children: React.ReactNode; +}) { + const router = useRouter(); + const pathname = usePathname(); + const alert = useAlertStore((state) => state.setMessage); + const setBbsInfo = useMenuStore((state) => state.setBbsInfo); + + const { data: boardInfo, isLoading } = useQuery({ + queryKey: [bbsId], + queryFn: () => BoardApi.getBoardInfo(bbsId), + }); + + useEffect(() => { + if (!boardInfo) return; + setBbsInfo(boardInfo); + }, [boardInfo]); + + if (!isLoading && pathname && boardInfo) { + if (!pathname.includes(boardInfo.bbsTypeCd.toLowerCase())) { + alert("잘못된 경로로 접속하였습니다.", router.back); + return null; + } + } + + return children; +} + +export { PostPageGuard }; diff --git a/apps/cm/src/entities/post/model/delete-reason-messages.ts b/apps/cm/src/entities/post/model/delete-reason-messages.ts new file mode 100644 index 000000000..3f1587472 --- /dev/null +++ b/apps/cm/src/entities/post/model/delete-reason-messages.ts @@ -0,0 +1,11 @@ +export const deleteReasonMessages = { + "저작권 침해": + "타인의 저작물을 허락 없이 게시하는 경우 저작권 침해로 간주되어 삭제될 수 있습니다.", + 명예훼손: "타인의 명예를 훼손하는 내용의 게시물은 삭제될 수 있습니다.", + "개인정보 노출": "타인의 개인정보를 노출하는 게시물도 삭제될 수 있습니다.", + "불법 정보 유통": + "불법적인 상품이나 서비스를 판매하거나 홍보하는 게시물은 삭제될 수 있습니다.", + "욕설 및 비방": + "심한 욕설이나 비방, 모욕적인 내용이 포함된 게시물은 삭제될 수 있습니다.", + "광고성 정보": "지나치게 광고성이 짙은 게시물은 삭제될 수 있습니다.", +}; diff --git a/apps/cm/src/entities/post/model/post-interface.ts b/apps/cm/src/entities/post/model/post-interface.ts new file mode 100644 index 000000000..1be87dadd --- /dev/null +++ b/apps/cm/src/entities/post/model/post-interface.ts @@ -0,0 +1,119 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface PostParams extends PageableParams { + keywordType?: string; + keyword?: string; + popupYn?: string; + upndFixYn?: string; + prdPstgYn?: string; + siteId?: string; + bbsTypeCd?: string; + ansUseYn?: string; +} + +export interface MyPostParams extends PageableParams { + keywordType?: string; + keyword?: string; + bbsId?: number | null; + delYn?: string; + frDateYmd?: string; + toDateYmd?: string; +} + +export interface Post { + rowNo: number; + bbsId: number; + pstNo: number; + pstTtl: string; + pstCn: string; + inqNocs: number; + rfrncUrl: string; + popupYn: string; + upndFixYn: string; + prdPstgYn: string; + rootPath: string; + pstPath: string; + ansPstn: number; + wrtrNm: string; + wrtrCplc: string; + atflCd: string; + regDt: string; + rgtrNm: string; + cmntCnt: number; +} + +export interface MyPost extends Post { + bbsNm: string; + bbsTypeCd: string; +} + +export interface PostInfo extends Post { + pstCn: string; + rgtr: string; + mdfcnDt: string; + mdfr: string; +} + +export interface MovePost { + fromBbsId: number; + toBbsId?: number; + pstNo: number; +} + +export interface DeletePost { + bbsId: number; + pstNo: number; + delRsn?: string; +} + +const postEtcSchema = z.object({ + bbsPstgTypeCd: z.string(), + pstgPrdSeCd: z.string(), + pstgBgngDt: z.string().nullish(), + pstgEndDt: z.string().nullish(), +}); + +export const postFormSchema = z.object({ + bbsId: z.number(), + pstTtl: z.string().nonempty(), + pstCn: z.string().nullish(), + rfrncUrl: z.string().nullish(), + popupYn: z.string().nullish(), + popupYnEtc: z.array(postEtcSchema).nullish(), + upndFixYn: z.string().nullish(), + upndFixYnEtc: z.array(postEtcSchema).nullish(), + prdPstgYn: z.string().nullish(), + prdPstgYnEtc: z.array(postEtcSchema).nullish(), + pstPswd: z.string(), + wrtrNm: z.string().nullish(), + wrtrCplc: z.string().nullish(), + atflCd: z.string().nullish(), + parentPstNo: z.number().nullish(), +}); + +export const postUpdateFormSchema = postFormSchema.omit({ + bbsId: true, + parentPstNo: true, +}); + +export type PostList = PageableList; +export type MyPostList = PageableList; +export type PostFormData = z.infer; +export type PostUpdateFormData = z.infer; + +export interface PostService { + getPostList(bbsId: number, params: PostParams): Promise; + getMyPostList(params: MyPostParams): Promise; + getPostInfo(bbsId: number, pstNo: number): Promise; + createPost(data: PostFormData): Promise; + updatePost( + bbsId: number, + pstNo: number, + data: PostUpdateFormData, + ): Promise; + deletePost(bbsId: number, pstNo: number, delRsn?: string): Promise; + softDeletePost(data: DeletePost[]): Promise; + hardDeletePost(data: DeletePost[]): Promise; + movePost(data: MovePost[]): Promise; +} diff --git a/apps/cm/src/pages/dashboard/api/dashboard-service.ts b/apps/cm/src/pages/dashboard/api/dashboard-service.ts new file mode 100644 index 000000000..94bf807a1 --- /dev/null +++ b/apps/cm/src/pages/dashboard/api/dashboard-service.ts @@ -0,0 +1,14 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { Dashboard, DashboardService } from "../model/dashboard-interface"; + +export class DashboardApiService implements DashboardService { + public async getDashboard(): Promise { + const response = await client.get( + ENDPOINT.CMS_SERVICE.DASHBOARD, + ); + return response.data; + } +} + +export const DashboardApi = new DashboardApiService(); diff --git a/apps/cm/src/pages/dashboard/lib/KoreaBasinMap.module.css b/apps/cm/src/pages/dashboard/lib/KoreaBasinMap.module.css new file mode 100644 index 000000000..0af00a62c --- /dev/null +++ b/apps/cm/src/pages/dashboard/lib/KoreaBasinMap.module.css @@ -0,0 +1,212 @@ +#mapArea { + display: flex; + align-items: center; + justify-content: center; +} + +.stage { + --map-inset: 7.5%; + aspect-ratio: 0.72/1; + width: 100%; + max-height: 100%; + position: relative; + margin: auto; +} + +.bgLayer { + position: absolute; + inset: var(--map-inset); + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + width: calc(100% - var(--map-inset) * 2); + height: calc(100% - var(--map-inset) * 2); + background-image: url("/images/region_map_bg.png"); + background-repeat: no-repeat; + background-size: contain; + background-position: center center; + image-rendering: -webkit-optimize-contrast; + image-rendering: crisp-edges; + image-rendering: pixelated; + opacity: 0.7; + filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.02)); + pointer-events: none; + min-height: 200px; +} + +.svgLayer { + position: absolute; + inset: var(--map-inset); + display: flex; + align-items: center; + justify-content: center; + z-index: 2; + width: calc(100% - var(--map-inset) * 2); + height: calc(100% - var(--map-inset) * 2); +} + +.svgLayer > svg { + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + image-rendering: -webkit-optimize-contrast; + image-rendering: crisp-edges; + shape-rendering: geometricPrecision; +} + +/* SVG 스타일을 위한 컨테이너 */ +.svgContainer :global(svg) :global(path) { + fill: rgba(0, 150, 255, 0.2) !important; + stroke: #0066cc; + stroke-width: 1; + vector-effect: non-scaling-stroke; +} + +.svgContainer :global(svg) :global(.cls-1) { + fill: rgba(0, 150, 255, 0.2) !important; + stroke: #0066cc; + stroke-width: 1; + vector-effect: non-scaling-stroke; +} + +.svgContainer :global(svg) :global(.basin) { + opacity: 0; + pointer-events: all; + transition: opacity 0.15s ease; + cursor: pointer; +} + +.svgContainer :global(svg) :global(.basin.active), +.svgContainer :global(svg) :global(.basin:hover) { + opacity: 1; +} + +.svgContainer :global(svg) :global(.basin) :global(path), +.svgContainer :global(svg) :global(.basin) :global(polygon), +.svgContainer :global(svg) :global(.basin) :global(polyline) { + fill: rgba(0, 209, 209, 0.5); + stroke: #399AC3; + stroke-width: 1.3; + vector-effect: non-scaling-stroke; +} + +/* 팝업 + 배지 */ +.popup { + position: absolute; + width: 300px; + height: 150px; + min-width: 220px; + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 10px; + box-shadow: + 0 12px 24px rgba(0, 0, 0, 0.18), + 0 2px 0 rgba(0, 0, 0, 0.05); + opacity: 0; + visibility: hidden; + transform: translate(-50%, -8px) scale(0.98); + transition: + opacity 0.12s, + visibility 0.12s, + transform 0.12s; + pointer-events: none; + z-index: 10; +} +.popup.show { + opacity: 1 !important; + visibility: visible !important; + transform: translate(-50%, -12px) scale(1) !important; +} + +.show { + opacity: 1; + visibility: visible; +} +.popup .head { + display: grid; + grid-template-columns: 1fr 1fr; + background: #d1ecfa; + border-bottom: 1px solid #c8e4f3; + font-weight: 700; + font-size: 16px; +} +.popup .head > div { + padding: 10px 12px; + text-align: center; +} +.popup .body { + padding: 6px 10px 8px; +} +.popup td { + padding: 8px 10px; + border-bottom: 1px solid #eef2f7; + font-size: 14px; +} +.popup .arrow { + position: absolute; + left: 50%; + bottom: -8px; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-top: 10px solid #fff; + filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.1)); +} +.badge { + position: absolute; + z-index: 9; + padding: 6px 10px; + border-radius: 8px; + background: #1e40af; + color: #fff; + font-weight: 700; + transform: translate(-50%, -50%); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.22); + pointer-events: none; + opacity: 0; + visibility: hidden; + transition: opacity 0.12s, visibility 0.12s; +} +.badge.show { + opacity: 1; + visibility: visible; +} + +/* 권역 라벨 */ +.labelsLayer { + position: absolute; + inset: var(--map-inset); + pointer-events: none; + z-index: 3; + width: 100%; + height: 100%; +} +.basinLabel { + position: absolute; + transform: translate(-50%, -50%); + pointer-events: none; + padding: 6px 12px; + border-radius: 6px; + font-weight: 700; + font-size: 16px; + background: #ffffffee; + border: 2px solid #d0d0d0; + white-space: nowrap; + min-width: 60px; + text-align: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} +.basinLabel.active { + background: #006bf8; + color: #fff; + border-color: #96c3ff; + box-shadow: 0 4px 12px rgba(0, 107, 248, 0.3); +} diff --git a/apps/cm/src/pages/dashboard/lib/KoreaBasinMap.tsx b/apps/cm/src/pages/dashboard/lib/KoreaBasinMap.tsx new file mode 100644 index 000000000..139f9f872 --- /dev/null +++ b/apps/cm/src/pages/dashboard/lib/KoreaBasinMap.tsx @@ -0,0 +1,425 @@ +import React, { useEffect, useRef } from "react"; +import styles from "./KoreaBasinMap.module.css"; // 추가 + +const SVG_URL = "/images/region_map.svg"; // public 폴더에 region_map.svg 위치 필요 + +const MANUAL_LABEL_POS = { + han: { x: 0.52, y: 0.14 }, + geum: { x: 0.28, y: 0.47 }, + nakdong: { x: 0.64, y: 0.47 }, + yeongsan: { x: 0.18, y: 0.77 }, +}; + +const BASIN_DATA = { + han: { + name: "한강", + colTitle: "시설수", + rows: [ + ["500m³ 미만", 100000000], + ["500m³ 이상", 100000000], + ], + }, + geum: { + name: "금강", + colTitle: "시설수", + rows: [ + ["500m³ 미만", 100000000], + ["500m³ 이상", 100000000], + ], + }, + nakdong: { + name: "낙동강", + colTitle: "시설수", + rows: [ + ["500m³ 미만", 100000000], + ["500m³ 이상", 100000000], + ], + }, + yeongsan: { + name: "영산강", + colTitle: "시설수", + rows: [ + ["500m³ 미만", 100000000], + ["500m³ 이상", 100000000], + ], + }, +}; + +const BASIN_IDS = [ + { key: "han", labels: ["한강", "han"] }, + { key: "geum", labels: ["금강", "geum"] }, + { key: "nakdong", labels: ["낙동강", "nakdong"] }, + { key: "yeongsan", labels: ["영산강", "yeongsan"] }, +]; + +function fmt(n: number) { + return new Intl.NumberFormat("ko-KR").format(n); +} + +export function KoreaBasinMap() { + const stageRef = useRef(null); + const svgLayerRef = useRef(null); + const labelsLayerRef = useRef(null); + const popupRef = useRef(null); + const badgeRef = useRef(null); + const rowsRef = useRef(null); + const colTitleRef = useRef(null); + + useEffect(() => { + let svg: SVGSVGElement | null = null; + let gs: SVGGElement[] = []; + let labels: HTMLDivElement[] = []; + + console.log("Loading SVG from:", SVG_URL); + fetch(SVG_URL) + .then((r) => { + console.log("SVG fetch response:", r.status, r.statusText); + if (!r.ok) throw new Error(`HTTP error! status: ${r.status}`); + return r.text(); + }) + .then((txt) => { + console.log("SVG content loaded, length:", txt.length); + if (!svgLayerRef.current) { + console.error("svgLayerRef.current is null"); + return; + } + svgLayerRef.current.innerHTML = txt; + svg = svgLayerRef.current.querySelector("svg"); + if (!svg) { + console.error("SVG element not found in loaded content"); + return; + } + console.log("SVG element found:", svg); + + // viewBox/크기 세팅 + if (!svg.hasAttribute("viewBox")) { + const bb = svg.getBBox(); + svg.setAttribute( + "viewBox", + [bb.x, bb.y, bb.width, bb.height].join(" "), + ); + } + const vb = (svg.getAttribute("viewBox") || "").split(/\s+/).map(Number); + if (vb.length === 4 && vb[3]! > 0 && stageRef.current) { + stageRef.current!.style.aspectRatio = String(vb[2]! / vb[3]!); + } + svg!.setAttribute("preserveAspectRatio", "xMidYMid meet"); + svg.style.width = "100%"; + svg.style.height = "100%"; + + // 1) 지도 그룹 수집 + gs = []; + console.log("Looking for basin groups in SVG..."); + BASIN_IDS.forEach((item) => { + let g: SVGGElement | null = null; + for (const id of item.labels) { + g = svg!.getElementById(id) as SVGGElement | null; + if (g) { + console.log(`Found group for ${item.key} with id '${id}':`, g); + break; + } + } + if (!g) { + console.warn(`No group found for ${item.key} with labels:`, item.labels); + return; + } + g.classList.add("basin"); + (g as SVGGElement & { dataset: { key: string; label: string } }).dataset.key = item.key; + (g as SVGGElement & { dataset: { key: string; label: string } }).dataset.label = item.labels[0] || ''; + + // 패스에 기본 스타일 적용 + const paths = g.querySelectorAll('path'); + paths.forEach((path) => { + path.style.fill = 'rgba(0, 150, 255, 0.2)'; + path.style.stroke = '#0066cc'; + path.style.strokeWidth = '1'; + path.style.vectorEffect = 'non-scaling-stroke'; + }); + + gs.push(g); + }); + console.log(`Found ${gs.length} basin groups`); + + // 각 유역 그룹의 패스 정보 확인 + gs.forEach((g, index) => { + const paths = g.querySelectorAll('path'); + console.log(`Group ${index} (${(g as SVGGElement & { dataset: { key: string } }).dataset.key}):`, { + element: g, + pathCount: paths.length, + paths: Array.from(paths).map(p => ({ + className: p.className.baseVal, + d: p.getAttribute('d')?.substring(0, 50) + '...' + })) + }); + }); + + // 2) 함수들 먼저 정의 + const clear = () => { + gs.forEach((el) => el.classList.remove("active")); + if (popupRef.current) { + if (styles.show) { + popupRef.current.classList.remove(styles.show); + } + popupRef.current.style.opacity = "0"; + popupRef.current.style.visibility = "hidden"; + } + if (badgeRef.current) { + if (styles.show) { + badgeRef.current.classList.remove(styles.show); + } + badgeRef.current.style.opacity = "0"; + badgeRef.current.style.visibility = "hidden"; + } + labels.forEach((l) => l.classList.remove("active")); + }; + + function showFor(g: SVGGElement) { + console.log("showFor called for:", g); + const key = (g as SVGGElement & { dataset: { key: string; label: string } }).dataset.key; + const data = (BASIN_DATA as Record)[key] || { + name: (g as SVGGElement & { dataset: { label: string } }).dataset.label, + colTitle: "", + rows: [], + }; + console.log("Data for popup:", data); + + if (badgeRef.current) badgeRef.current.textContent = data.name; + if (colTitleRef.current) + colTitleRef.current.textContent = data.colTitle || "???"; + if (rowsRef.current) + rowsRef.current.innerHTML = data.rows + .map( + (r: (string | number)[]) => + `${r[0]}${fmt(Number(r[1]) || 0)}`, + ) + .join(""); + + if (!stageRef.current) return; + const sRect = stageRef.current.getBoundingClientRect(), + r = g.getBoundingClientRect(); + const cx = (r.left + r.right) / 2 - sRect.left; + const ty = r.top - sRect.top; + + console.log("Popup position:", { cx, ty }); + + if (badgeRef.current) { + badgeRef.current.style.left = cx + "px"; + badgeRef.current.style.top = ty + 24 + "px"; + } + if (popupRef.current) { + popupRef.current.style.left = cx + "px"; + popupRef.current.style.top = ty + 6 + "px"; + popupRef.current.style.opacity = "1"; + popupRef.current.style.visibility = "visible"; + if (styles.show) { + popupRef.current.classList.add(styles.show); + } + console.log("Added show class to popup:", popupRef.current.classList.toString()); + } + if (badgeRef.current && styles.show) { + badgeRef.current.classList.add(styles.show); + } + + if (popupRef.current) { + const pRect = popupRef.current.getBoundingClientRect(); + const x = Math.max( + 12 + pRect.width / 2, + Math.min(cx, sRect.width - 12 - pRect.width / 2), + ); + popupRef.current.style.left = x + "px"; + if (badgeRef.current) badgeRef.current.style.left = x + "px"; + } + } + + function showForWithLabel(g: SVGGElement) { + console.log("showForWithLabel called for:", g); + showFor(g); + const idx = gs.indexOf(g); + labels.forEach((l) => l.classList.remove("active")); + if (idx >= 0) labels[idx]!.classList.add("active"); + } + + // 3) 라벨 DOM 생성 + labels = []; + if (labelsLayerRef.current) { + labelsLayerRef.current.innerHTML = ""; + gs.forEach((g) => { + const el = document.createElement("div"); + el.className = styles.basinLabel || ''; + el.textContent = (g as SVGGElement & { dataset: { label: string } }).dataset.label || ''; + labelsLayerRef.current!.appendChild(el); + labels.push(el); + }); + } + + // 3) 라벨 위치 + function positionLabels() { + if (!stageRef.current || !svg) return; + const sRect = stageRef.current.getBoundingClientRect(); + const inset = + parseFloat( + getComputedStyle(stageRef.current).getPropertyValue( + "--map-inset", + ) || "0", + ) / 100; + const left = sRect.width * inset, + top = sRect.height * inset; + const w = sRect.width * (1 - inset * 2), + h = sRect.height * (1 - inset * 2); + const [vx, vy, vw, vh] = ( + svg!.getAttribute("viewBox") || "0 0 100 100" + ) + .split(/\s+/) + .map(Number); + + const svgRect = svg.getBoundingClientRect(); + const stageRect = stageRef.current.getBoundingClientRect(); + const offsetX = svgRect.left - stageRect.left - left; + const offsetY = svgRect.top - stageRect.top - top; + + gs.forEach((g, i) => { + const key = (g as SVGGElement & { dataset: { key: string } }).dataset.key; + const manual = (MANUAL_LABEL_POS as Record)[key]; + let px, py; + if ( + manual && + (("x" in manual && "y" in manual) || + ("px" in manual && "py" in manual)) + ) { + if ("px" in manual) { + px = left + (manual.px || 0) + offsetX; + py = top + (manual.py || 0) + offsetY; + } else { + px = left + w * (manual.x || 0) + offsetX; + py = top + h * (manual.y || 0) + offsetY; + } + } else { + const b = g.getBBox(); + const cx = b.x + b.width / 2, + cy = b.y + b.height / 2; + px = ((cx - vx!) / vw!) * w + left + offsetX; + py = ((cy - vy!) / vh!) * h + top + offsetY; + } + labels[i]!.style.left = `${px}px`; + labels[i]!.style.top = `${py + 18}px`; + }); + } + + // 4) 상호작용 (함수들은 이미 위에서 정의됨) + gs.forEach((g) => { + g.addEventListener("mouseenter", () => { + clear(); + g.classList.add("active"); + showForWithLabel(g); + }); + g.addEventListener("mousemove", () => { + if (g.classList.contains("active")) showForWithLabel(g); + }); + g.addEventListener("mouseleave", clear); + g.addEventListener("click", (e) => { + e.stopPropagation(); + clear(); + g.classList.add("active"); + showForWithLabel(g); + }); + }); + if (stageRef.current) { + stageRef.current.addEventListener("mouseleave", clear); + stageRef.current.addEventListener("click", () => clear()); + } + + // 5) 실행 + positionLabels(); + window.addEventListener("resize", positionLabels); + new window.ResizeObserver(positionLabels).observe(stageRef.current!); + }) + .catch((error) => { + console.error("Error loading SVG:", error); + if (svgLayerRef.current) + svgLayerRef.current.innerHTML = + `
지도를 불러올 수 없습니다. (${error.message})
`; + }); + + // cleanup + return () => { + window.removeEventListener("resize", () => {}); + }; + }, []); + + // 스타일은 필요에 따라 tailwind, css 등으로 커스터마이즈하세요. + return ( +
+
+
+
+
+ +
+ ); +} diff --git a/apps/cm/src/pages/dashboard/model/chart-data.ts b/apps/cm/src/pages/dashboard/model/chart-data.ts new file mode 100644 index 000000000..0d55f9bc7 --- /dev/null +++ b/apps/cm/src/pages/dashboard/model/chart-data.ts @@ -0,0 +1,172 @@ +import { type ChartData, type ChartOptions } from "chart.js"; + +const commonData = { + labels: ["1", "2", "3", "4", "5", "6", "7"], + datasets: [ + { + fill: true, + pointRadius: 0, + tension: 0.5, + }, + ], +}; + +const commonOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false, + }, + }, + scales: { + x: { + display: false, + }, + y: { + display: false, + }, + }, +}; + +export const chartData: Record< + string, + { data: ChartData<"line" | "bar">; options: ChartOptions<"line" | "bar"> } +> = { + memberChartData: { + data: { + ...commonData, + datasets: [ + { + ...commonData.datasets[0], + data: [8, 10, 7, 12, 9, 13, 12], + borderColor: "rgba(255, 255, 255, 0.6)", + backgroundColor: "rgba(255, 255, 255, 0.1)", + }, + ], + }, + options: { + ...commonOptions, + scales: { + ...commonOptions.scales, + y: { + ...commonOptions.scales.y, + min: 5, + max: 15, + }, + }, + }, + }, + postChartData: { + data: { + ...commonData, + datasets: [ + { + ...commonData.datasets[0], + data: [5200, 5350, 5450, 5520, 5600, 5650, 5678], + borderColor: "rgba(86, 138, 255, 1)", + backgroundColor: "rgba(86, 138, 255, 0.1)", + }, + ], + }, + options: { + ...commonOptions, + }, + }, + visitorChartData: { + data: { + ...commonData, + datasets: [ + { + ...commonData.datasets[0], + data: [1800, 2100, 1950, 2300, 2150, 2250, 2345], + borderColor: "rgba(149, 131, 252)", + backgroundColor: "rgba(149, 131, 252, 0.1)", + }, + ], + }, + options: { + ...commonOptions, + }, + }, + apiChartData: { + data: { + ...commonData, + datasets: [ + { + ...commonData.datasets[0], + data: [41000, 42500, 43200, 44100, 44800, 45200, 45678], + borderColor: "rgba(220, 103, 206)", + backgroundColor: "rgba(220, 103, 206, 0.1)", + }, + ], + }, + options: { + ...commonOptions, + }, + }, + siteAccessChartData: { + data: { + labels: Array.from({ length: 24 }, (_, i) => + i.toString().padStart(2, "0"), + ), + datasets: [], + }, + options: { + ...commonOptions, + plugins: { + legend: { + position: "top", + }, + tooltip: { + mode: "index", + intersect: false, + }, + }, + scales: { + x: { + title: { + text: "시간대", + }, + }, + y: { + title: { + text: "접속자 수", + }, + beginAtZero: true, + }, + }, + }, + }, + menuAccessChartData: { + data: { + labels: [], + datasets: [], + }, + options: { + ...commonOptions, + plugins: { + legend: { + position: "top", + }, + tooltip: { + mode: "index", + intersect: false, + }, + }, + scales: { + x: { + title: { + text: "메뉴명", + }, + }, + y: { + title: { + text: "접속 횟수", + }, + beginAtZero: true, + }, + }, + }, + }, +}; diff --git a/apps/cm/src/pages/dashboard/model/dashboard-interface.ts b/apps/cm/src/pages/dashboard/model/dashboard-interface.ts new file mode 100644 index 000000000..9e038eef7 --- /dev/null +++ b/apps/cm/src/pages/dashboard/model/dashboard-interface.ts @@ -0,0 +1,32 @@ +export interface SiteStatisticsContent { + siteId: string; + siteNm: string; + accessHour: string; + accessCnt: number; +} + +export interface MenuStatisticsContent { + menuCd: number; + menuNm: string; + accessCnt: number; +} + +export interface Dashboard { + memberTotCnt: number; + memberTodayCnt: number; + pstTotCnt: number; + pstTodayCnt: number; + accessTodayCnt: number; + accessBefDayPer: number; + apiTodayCnt: number; + waitMemberCnt: number; + waitCntnCnt: number; + notiList: []; + schdlList: []; + siteAccessList: SiteStatisticsContent[]; + menuAccessList: MenuStatisticsContent[]; +} + +export interface DashboardService { + getDashboard(): Promise; +} diff --git a/apps/cm/src/pages/dashboard/model/table-columns.tsx b/apps/cm/src/pages/dashboard/model/table-columns.tsx new file mode 100644 index 000000000..aac2b2868 --- /dev/null +++ b/apps/cm/src/pages/dashboard/model/table-columns.tsx @@ -0,0 +1,75 @@ +import dayjs from "dayjs"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Badge } from "@common/components"; +import type { Post, MyPost } from "@/entities/post"; + +const noticePostTableColumns: ColumnDef[] = [ + { + header: "번호", + cell: ({ row, table }) => { + if (row.original.upndFixYn === "Y") { + return ( + + + + ); + } + const totalRows = table.getRowCount(); + const currentPage = table.getState().pagination.pageIndex; + const pageSize = table.getState().pagination.pageSize; + return totalRows - (currentPage * pageSize + row.index); + }, + }, + { + accessorKey: "pstTtl", + header: "제목", + }, + { + accessorKey: "wrtrNm", + header: "작성자", + }, + { + accessorKey: "regDt", + header: "작성일", + cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"), + }, + { + accessorKey: "inqNocs", + header: "조회수", + }, +]; + +const myPostTableColumns: ColumnDef[] = [ + { + header: "번호", + cell: ({ row, table }) => { + const totalRows = table.getRowCount(); + const currentPage = table.getState().pagination.pageIndex; + const pageSize = table.getState().pagination.pageSize; + return totalRows - (currentPage * pageSize + row.index); + }, + }, + { + accessorKey: "pstTtl", + header: "제목", + }, + { + accessorKey: "wrtrNm", + header: "작성자", + }, + { + accessorKey: "regDt", + header: "작성일", + cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"), + }, + { + accessorKey: "inqNocs", + header: "조회수", + }, +]; + + +export { noticePostTableColumns, myPostTableColumns }; diff --git a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx index 075efac41..76b6876fb 100644 --- a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx @@ -1,9 +1,36 @@ "use client"; -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { tokenManager } from '@/entities/auth/lib/token-manager'; -import { client } from '@/shared/api'; -import { ENDPOINT } from '@/shared/config'; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { tokenManager } from "@/entities/auth/lib/token-manager"; +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import { KoreaBasinMap } from "../lib/KoreaBasinMap"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + Filler, + Legend, + Tooltip, + ArcElement, +} from "chart.js"; +import { Doughnut } from "react-chartjs-2"; + +// Chart.js 등록 +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + Filler, + Legend, + Tooltip, + ArcElement +); interface UserInfo { email?: string; @@ -14,124 +41,258 @@ interface UserInfo { function DashboardPage() { const [userInfo, setUserInfo] = useState(null); - const [error, setError] = useState(''); + const [error, setError] = useState(""); const router = useRouter(); useEffect(() => { - const fetchUserInfo = async () => { - console.log('fetchUserInfo called'); - + const fetchUserInfo = async () => { + console.log("fetchUserInfo called"); + try { // tokenManager를 통해 토큰 확인 - const accessToken = tokenManager.getToken('Authorization'); - const userId = tokenManager.getToken('token-id'); - + const accessToken = tokenManager.getToken("Authorization"); + const userId = tokenManager.getToken("token-id"); + if (!accessToken || !userId) { - console.log('No tokens found, redirecting to login'); - setError('로그인이 필요합니다.'); - router.push('/auth/login'); + console.log("No tokens found, redirecting to login"); + setError("로그인이 필요합니다."); + router.push("/auth/login"); return; } - console.log('Fetching user info with userId:', userId); - + console.log("Fetching user info with userId:", userId); + // client를 사용하여 API 호출 (자동 토큰 처리) - const response = await client.get(`${ENDPOINT.USER_SERVICE.USERS}/${userId}`); - + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/${userId}`, + ); + if (response.data) { - console.log('User info received:', response.data); + console.log("User info received:", response.data); setUserInfo(response.data); } else { - setError('사용자 정보를 불러올 수 없습니다.'); + setError("사용자 정보를 불러올 수 없습니다."); } } catch (error) { - console.error('Error fetching user info:', error); - if (error instanceof Error && error.message.includes('401')) { - setError('인증이 만료되었습니다. 다시 로그인해주세요.'); + console.error("Error fetching user info:", error); + if (error instanceof Error && error.message.includes("401")) { + setError("인증이 만료되었습니다. 다시 로그인해주세요."); tokenManager.clearAuthToken(); - router.push('/auth/login'); + router.push("/auth/login"); } else { - setError('네트워크 오류가 발생했습니다.'); + setError("네트워크 오류가 발생했습니다."); } } }; - + fetchUserInfo(); }, [router]); const handleLogout = () => { - console.log('Logging out...'); + console.log("Logging out..."); // localStorage 정리 - localStorage.removeItem('tempToken'); + localStorage.removeItem("tempToken"); // tokenManager를 통해 쿠키 정리 tokenManager.clearAuthToken(); - router.push('/auth/logout'); + router.push("/auth/logout"); }; - const handleChangePassword = () => { - router.push('/otp?next=change-password'); + const handleChangePassword = () => { + router.push("/otp?next=change-password"); }; return (
{/* 첫번째 라인 */} -
- {/* 총 회원수 카드 */} -
- - +
+ {/* 첫번쨰 열 */} +
+ {/* 전국 하수처리시설 가동률 */} +
+
+

전국 하수처리시설 가동률

+
+
+ { + const ctx = chart.ctx; + const centerX = chart.width / 2; + const centerY = chart.height / 2; + + // 가동률 텍스트 + ctx.save(); + ctx.font = 'bold 24px Arial'; + ctx.fillStyle = '#00DFE5'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('75%', centerX, centerY + 10); + + // 라벨 텍스트 + ctx.font = '12px Arial'; + ctx.fillStyle = '#666'; + ctx.fillText('가동률', centerX, centerY + 30); + ctx.restore(); + } + }]} + /> +
+
+ + {/* 오수관로 및 우수관로 현황 */} +
+
+

오수관로 및 우수관로 현황

+
+
+
+ + {/* 하수처리수 재이용 */} +
+
+

하수처리수 재이용

+
+
+
- {/* 전체 게시물 카드 */} -
+ + {/* 두번쨰 열 */} +
+ {/* 하수처리시설(개소) */} +
+
+

하수처리시설(개소)

+
+
+
+
+

500㎥ 미만(개소)

+
+
3,567 개소
+
+
+
+

500㎥ 이상(개소)

+
+
767 개소
+
+
기준년월 : 2025.09
+
+
+ {/* 노후하수관로현황 */} +
+
+

노후하수관로현황

+
+
+
+
+ + {/* 재이용시설(개소) */} +
+
+

재이용시설(개소)

+
+
+
+
- {/* 오늘 접속자 수 카드 */} -
- + + {/* 지도영역 */} +
+
+
+

유역별 정보

+
+ +
{/* 사용자 정보 카드 */} -
-

사용자 정보

- {error &&
{error}
} - {userInfo ? ( -
-
아이디: {userInfo.userId}
-
이메일: {userInfo.email || userInfo.userId}
-
이름: {userInfo.userName || '-'}
-
2FA 활성화: {userInfo.twoFactorEnabled ? '예' : '아니오'}
+
+

사용자 정보

+ {error &&
{error}
} + {userInfo ? ( +
+
+ 아이디: {userInfo.userId}
- ) : ( - !error &&
사용자 정보를 불러오는 중...
- )} - - -
+
+ 이메일: {userInfo.email || userInfo.userId} +
+
+ 이름: {userInfo.userName || "-"} +
+
+ 2FA 활성화:{" "} + {userInfo.twoFactorEnabled ? "예" : "아니오"} +
+
+ ) : ( + !error &&
사용자 정보를 불러오는 중...
+ )} + + +
{/* 두번째 라인 */} -
-
+
+
+
+

지원현황

+
+
+ +
+
+
+
+

자료관 자료현황

+
+
+
+
+
+
+

공지사항

+
+
+
-
-
-
+ {/* 배너 영역 */} +
- {/* 세번째 라인 */} -
-
-
-
-
-
- - {/* 네번째 라인 */} -
-
-
-
@@ -139,4 +300,4 @@ function DashboardPage() { ); } -export { DashboardPage }; \ No newline at end of file +export { DashboardPage }; diff --git a/apps/cm/src/shared/config/api.ts b/apps/cm/src/shared/config/api.ts index 36dfc69a3..25c8b88c9 100644 --- a/apps/cm/src/shared/config/api.ts +++ b/apps/cm/src/shared/config/api.ts @@ -69,6 +69,17 @@ export const ENDPOINT = { POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts", POST_EVALUATION: "/board-service/api/v1/pstEvl", }, + SEWERAGE_SERVICE: { + PPL_COUNTS: "/sewerage-service/api/v1/ms/pplCounts", + OLD_PPL_COUNTS: "/sewerage-service/api/v1/ms/oldPplCounts", + }, + IC_SERVICE: { + GROUPING_STATISTICS: "/ic-service/api/v1/nti/grouping-statistics", + }, + DC_SERVICE: { + DATA_STATUS: "/dc-service/api/v1/dc/dataStatus", + }, + }; export const JWT_EXPIRATION_TIME = 1800000; diff --git a/apps/dm/.env.development b/apps/dm/.env.development index 7a7dc3f52..833207ed0 100644 --- a/apps/dm/.env.development +++ b/apps/dm/.env.development @@ -1,22 +1,22 @@ +# 공단 개발 NEXT_PUBLIC_SITE_ID = DM -NEXT_PUBLIC_PORT = 11035 -NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035 -NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 -NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 -NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 -NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_PORT = 3000 +NEXT_PUBLIC_PROXY_HOST = https://devreuse.hasuon.or.kr +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token -NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1 -NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g -NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API -# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성 -#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정 +NEXT_PUBLIC_REUSE_API_URL= http://apigateway.iipbs-was.svc.cluster.local:8000/reuse-service/api/v1 +NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC +NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/sewerage-service/api NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do -NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM= +NEXT_PRIVATE_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA= diff --git a/apps/dm/.env.local b/apps/dm/.env.local index 3c390c8bd..c43f5af55 100644 --- a/apps/dm/.env.local +++ b/apps/dm/.env.local @@ -5,7 +5,7 @@ NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token NEXT_PUBLIC_PORT = 3005 #NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035 -NEXT_PUBLIC_PROXY_HOST = http://localhost:3005/ +NEXT_PUBLIC_PROXY_HOST = http://localhost:3005 NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 diff --git a/apps/dm/.env.mindone b/apps/dm/.env.mindone new file mode 100644 index 000000000..7a7dc3f52 --- /dev/null +++ b/apps/dm/.env.mindone @@ -0,0 +1,22 @@ +NEXT_PUBLIC_SITE_ID = DM +NEXT_PUBLIC_PORT = 11035 +NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035 +NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token +NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g +NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API +# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성 +#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정 + + + +NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do +NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM= diff --git a/apps/dm/.env.production b/apps/dm/.env.production index b9d81ccc4..e627195cb 100644 --- a/apps/dm/.env.production +++ b/apps/dm/.env.production @@ -1,9 +1,10 @@ +# 공단 운영 NEXT_PUBLIC_SITE_ID = DM -NEXT_PUBLIC_PORT = 11035 -NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035 -NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/api/v1 -NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/operation-service/api/v1 -NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000 +NEXT_PUBLIC_PORT = 3000 +NEXT_PUBLIC_PROXY_HOST = https://reuse.hasuon.or.kr +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl @@ -11,9 +12,9 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token -NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_REUSE_API_URL= http://apigateway.iipbs-was.svc.cluster.local:8000/reuse-service/api/v1 NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC -NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api +NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/sewerage-service/api diff --git a/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx b/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx index f5acfdb97..ce38a1226 100644 --- a/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx +++ b/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx @@ -5,7 +5,9 @@ import { useDmStore } from '@/app/store'; import type { DmStatusDataType } from '@/app/store/wrStatus/type'; import { CHART_FORM, MainChart, type ChartConfig } from '@common/business_components'; import React, { useEffect, useRef, useState, useCallback } from 'react'; +import { setCookie } from "../../../../src/shared/lib"; +setCookie("site-id-key", process.env.NEXT_PUBLIC_SITE_ID || "DM"); interface WrStatusChartProps { chartForm: string; diff --git a/apps/dm/app/(popup-layout)/map/service/map.ts b/apps/dm/app/(popup-layout)/map/service/map.ts index aa98a0bc2..9c10ca190 100644 --- a/apps/dm/app/(popup-layout)/map/service/map.ts +++ b/apps/dm/app/(popup-layout)/map/service/map.ts @@ -1,9 +1,7 @@ - /** * 관망도 API */ - const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/map` - const REUSE_API_URL = `${process.env.NEXT_PUBLIC_REUSE_API_URL}` +import { DEV_URL_WR, DEV_URL_SN } from "@/components/ui/config"; export type LyrStngProps = { lyr_sn: string @@ -31,7 +29,7 @@ export type LyrStngProps = { export const mapService = { //레이어 설정 조회 getLyrStngs: async () => { - const response = await fetch(`${API_URL}/lyrStngs/DM_NETWORK`, { + const response = await fetch(`${DEV_URL_SN}/app/v1/map/lyrStngs/DM_NETWORK`, { method: 'GET', headers: { 'Accept': 'application/json', @@ -45,7 +43,7 @@ export const mapService = { return response.json(); }, getPbnstRuseFclts: async (ruseFcltPk:any) => { - const response = await fetch(`${REUSE_API_URL}/reuseFc/pubFc/${ruseFcltPk}`, { + const response = await fetch(`${DEV_URL_WR}/api/v1/reuseFc/pubFc/${ruseFcltPk}`, { method: 'GET', headers: { 'Accept': 'application/json', diff --git a/apps/dm/app/middleware.ts b/apps/dm/app/middleware.ts deleted file mode 100644 index ad35dfba7..000000000 --- a/apps/dm/app/middleware.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; -const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; -const SERVER_API_URL = process.env.NEXT_PUBLIC_DM_SERVER_API_URL || ""; -const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ""; -const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || ""; -const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || ""; -const DC_URL = process.env.NEXT_PUBLIC_DC_URL || ""; - -export function middleware(req: NextRequest) { - console.log("Middleware 진입:", req.nextUrl.pathname); - const pathname = req.nextUrl.pathname; - const searchParams = req.nextUrl.searchParams; - - if (pathname.startsWith("/server")) { - - const decodedParams = Array.from(searchParams.entries()) - .map(([key, value]) => `${key}=${decodeURIComponent(value)}`) - .join("&"); - - const newUrl = `${SERVER_API_URL}${pathname.replace("/server", "")}${ - decodedParams ? `?${decodedParams}` : "" - }`; - return NextResponse.rewrite(new URL(newUrl)); - } - - if (pathname.startsWith("/geoserver")) { - let queryParams = searchParams.toString(); - const typename = searchParams.get("TYPENAME"); - - let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; - - if ( - typename == "hasudo:tb_il_swage_prcs_fclt" //하수처리시설 - || typename == "hasudo:tb_ms_swage_prcs_zone" //하수처리구역 - || typename == "hasudo:tb_ms_sbmrg_dngr_map" //침수위험지도 - || typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도 - ) { - newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; - } - else if ( - typename == "hasudo:tb_ms_swage_ppl" //관로 - || typename == "hasudo:tb_ms_swage_mhl" //맨홀 - || typename == "hasudo:tb_ms_swage_pmstn" //펌프장 - || typename == "hasudo:tb_ms_stofcbr" //우수토실 - ) { - newUrl = `${LX_GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; - } - else if ( - typename == "hasudo:om_dt_untrt_swg_rain" //중점우수토실 - || typename == "hasudo:om_wqlt_station" //관측소 - || typename == "hasudo:tb_wr_bdst_ruse_fclt" //재이용시설 - || typename == "hasudo:tb_wr_rnwtr_utztn_fclt" //빗물이용시설 - || typename == "hasudo:tb_wr_rswtr_fclt" //중수도시설 - || typename == "hasudo:tb_wr_htdfl_fclt" //온배수시설 - || typename == "hasudo:tb_wr_ruse_fclt_dmd_co" //수요처 - || typename == "hasudo:tb_wr_tmpr_dmd_co" //잠재수요처 - ){ - const url = req.nextUrl.clone(); - url.searchParams.set('TYPENAME', typename.replace("hasudo:","")); - queryParams = url.searchParams.toString(); - newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`; - } - - console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); - return NextResponse.rewrite(new URL(newUrl)); - } - - if ( - pathname.startsWith("/attach-service") || - pathname.startsWith("/cms-service") - ) { - const requestHeaders = new Headers(req.headers); - const token = req.cookies.get(CLAIM_NAME)?.value; - const userId = req.cookies.get(AUTH_USER_ID)?.value; - const newUrl = `${SERVER_API_URL}${pathname}`; - - if (token) requestHeaders.set(CLAIM_NAME, token); - if (userId) requestHeaders.set(AUTH_USER_ID, userId); - - console.log( - `[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`, - ); - return NextResponse.rewrite(new URL(newUrl), { - headers: requestHeaders, - }); - } - - if (pathname.startsWith("/user-service")) { - const requestHeaders = new Headers(req.headers); - const newUrl = `${SERVER_API_URL}${pathname}`; - - console.log( - `[Middleware] Rewriting from '/user-service' path to: ${newUrl}`, - ); - return NextResponse.rewrite(new URL(newUrl), { - headers: requestHeaders, - }); - } - - if (pathname.startsWith("/dc-service")) { - const requestHeaders = new Headers(req.headers); - const decodedParams = Array.from(searchParams.entries()) - .map(([key, value]) => `${key}=${decodeURIComponent(value)}`) - .join("&"); - - const newUrl = `${DC_URL}${pathname}${decodedParams ? `?${decodedParams}` : ""}`; - console.log( - `[Middleware] Rewriting from '/dc-service' path to: ${newUrl}`, - ); - return NextResponse.rewrite(new URL(newUrl), { - headers: requestHeaders, - }); - } - return NextResponse.next(); -} - -export const config = { - matcher: [ - "/server/:path*", - "/geoserver/:path*", - "/attach-service/:path*", - "/user-service/:path*", - "/cms-service/:path*", - "/dc-service/:path*", - ], - -}; diff --git a/apps/dm/app/store/map/index.tsx b/apps/dm/app/store/map/index.tsx index 5ed066f73..f5b44c891 100644 --- a/apps/dm/app/store/map/index.tsx +++ b/apps/dm/app/store/map/index.tsx @@ -1,7 +1,12 @@ +import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config"; import { fetchType, getFetch } from "@/components/ui/api"; import { DEV_URL_FO } from "@/components/ui/config"; +import { getCookie } from "@common/components/lib/cookie"; import type { FacilityInfo, SubsInfo } from "./type"; +const tokenId = getCookie(AUTH_USER_ID); +const Authorization = getCookie(CLAIM_NAME); + const checkWrRegList = async () => { const wrRegList = await getFetch(fetchType.WR, `/reuseFc/pubFc`); return wrRegList; @@ -26,6 +31,8 @@ const getRuseFclt = async ( method: "POST", headers: { "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(ruseList), }); diff --git a/apps/dm/components/ui/api/index.ts b/apps/dm/components/ui/api/index.ts index 28644b00e..7ba66c4ed 100644 --- a/apps/dm/components/ui/api/index.ts +++ b/apps/dm/components/ui/api/index.ts @@ -1,5 +1,10 @@ //8880 +import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config"; import { DEV_URL_FO, DEV_URL_WR } from "../config"; +import { getCookie } from "@common/components/lib/cookie"; + +const tokenId = getCookie(AUTH_USER_ID); +const Authorization = getCookie(CLAIM_NAME); export const fetchType = { COMMON: "common", @@ -17,6 +22,8 @@ export const getFetch = async (type: string, url: string) => { method: "GET", headers: { accept: "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, }); @@ -32,6 +39,8 @@ export const postFetch = async (url: string, params: RequestParams) => { method: "POST", headers: { "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); @@ -47,6 +56,8 @@ export const putFetch = async (url: string, params: RequestParams) => { method: "PUT", headers: { "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); @@ -62,6 +73,8 @@ export const deleteFetch = async (url: string, params: RequestParams) => { method: "DELETE", headers: { "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); diff --git a/apps/dm/components/ui/config/index.ts b/apps/dm/components/ui/config/index.ts index 2ed4ef03d..1585bb767 100644 --- a/apps/dm/components/ui/config/index.ts +++ b/apps/dm/components/ui/config/index.ts @@ -1,5 +1,6 @@ -import { PROXY_HOST, SERVER_API_FO_URL, SERVER_API_WR_URL } from "@/src/shared/config/env"; +import { PROXY_HOST } from "@/src/shared/config/env"; export const LOCAL_URL = PROXY_HOST; -export const DEV_URL_FO = SERVER_API_FO_URL; -export const DEV_URL_WR = SERVER_API_WR_URL; +export const DEV_URL_FO = "/operation-service"; +export const DEV_URL_WR = "/reuse-service"; +export const DEV_URL_SN = "/sewerage-service"; diff --git a/apps/dm/middleware.ts b/apps/dm/middleware.ts index 4ac2e3d99..8616a6e80 100644 --- a/apps/dm/middleware.ts +++ b/apps/dm/middleware.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config"; +import { SERVER_API_FO_URL, SERVER_API_WR_URL } from "./src/shared/config/env"; // const CLAIM_NAME = "Authorization"; // const AUTH_USER_ID = "token-id"; @@ -115,12 +116,30 @@ export function middleware(req: NextRequest) { return NextResponse.rewrite(new URL(newUrl)); } - if (pathname.startsWith("/sewerage-service")) { + if (pathname.startsWith("/reuse-service")) { const queryParams = searchParams.toString(); - const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service/api", "")}${ + const newUrl = `${SERVER_API_WR_URL}${pathname.replace("/reuse-service", "")}${ queryParams ? `?${queryParams}` : "" }`; - console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); + console.log(`[Middleware] Rewriting from '/reuse-service' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname.startsWith("/operation-service")) { + const queryParams = searchParams.toString(); + const newUrl = `${SERVER_API_FO_URL}${pathname.replace("/operation-service", "")}${ + queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/operation-service' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname.startsWith("/sewerage-service")) { + const queryParams = searchParams.toString(); + const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service", "")}${ + queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/sewerage-service' path to: ${newUrl}`); return NextResponse.rewrite(new URL(newUrl)); } if (pathname.startsWith("/bussiness-juso-service")) { @@ -150,6 +169,8 @@ export const config = { "/user-service/:path*", "/attach-service/:path*", "/geoserver/:path*", + "/operation-service/:path*", + "/reuse-service/:path*", "/sewerage-service/:path*", "/bussiness-juso-service/:path*", "/", diff --git a/apps/dm/next.config.ts b/apps/dm/next.config.ts index 48fc8dacb..bc21e16a4 100644 --- a/apps/dm/next.config.ts +++ b/apps/dm/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const subDomain = - process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:3000"; + process.env.NEXT_PUBLIC_PROXY_HOST || "http://localhost:3000"; const nextConfig: NextConfig = { output: "standalone", @@ -22,8 +22,16 @@ const nextConfig: NextConfig = { ], afterFiles: [ { - source: "/pst/:path*", - destination: `${subDomain}/pst/:path*`, + source: "/sewerage-service/:path*", + destination: `${subDomain}/sewerage-service/:path*`, + }, + { + source: "/reuse-service/:path*", + destination: `${subDomain}/reuse-service/:path*`, + }, + { + source: "/operation-service/:path*", + destination: `${subDomain}/operation-service/:path*`, }, ], fallback: [], diff --git a/apps/dm/src/shared/api/client.ts b/apps/dm/src/shared/api/client.ts new file mode 100644 index 000000000..cf37e6671 --- /dev/null +++ b/apps/dm/src/shared/api/client.ts @@ -0,0 +1,11 @@ +import { Client } from "@common/network"; +import { BASE_URL } from "../config/api"; +import { SITE_ID } from "../config/env"; +import { setCookie } from "../lib"; + +setCookie("site-id-key", SITE_ID); + +export const client = Client.getInstance({ + baseURL: BASE_URL, + siteId: SITE_ID, +}); diff --git a/apps/dm/src/shared/api/index.ts b/apps/dm/src/shared/api/index.ts new file mode 100644 index 000000000..b7b9d6121 --- /dev/null +++ b/apps/dm/src/shared/api/index.ts @@ -0,0 +1 @@ +export { client } from "./client"; diff --git a/apps/dm/src/shared/lib/index.ts b/apps/dm/src/shared/lib/index.ts new file mode 100644 index 000000000..a5679f1b5 --- /dev/null +++ b/apps/dm/src/shared/lib/index.ts @@ -0,0 +1,8 @@ +export { getCookie, setCookie, removeCookie } from "./cookie"; +// export { useMutationWrapper } from "./use-mutation-wrapper"; +// export { useAlertStore } from "./use-alert-store"; +// export { useDataTable } from "./use-data-table"; +// export { useMenuStore } from "./use-menu-store"; +// export { useUserStore } from "./use-user-store"; +// export { usePrompt } from "./use-prompt"; +// export { useSelectPrompt } from "./use-select-prompt"; diff --git a/apps/fo/.env.development b/apps/fo/.env.development index de81dde66..ffaf99feb 100644 --- a/apps/fo/.env.development +++ b/apps/fo/.env.development @@ -1,18 +1,17 @@ +# 공단 개발 NEXT_PUBLIC_SITE_ID = FO -NEXT_PUBLIC_PORT = 11300 -NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11300 -NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 -#NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 -NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 -NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 -#NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver -NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver -NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +NEXT_PUBLIC_PORT = 3000 NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token -NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g -NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API - - +NEXT_PUBLIC_PROXY_HOST = https://devlarva.hasuon.or.kr +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service +NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl +NEXT_PUBLIC_WR_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/reuse-service/api/v1 +NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/sewerage-service/api +NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/dc-service \ No newline at end of file diff --git a/apps/fo/.env.local b/apps/fo/.env.local index 2e937af0c..1838377df 100644 --- a/apps/fo/.env.local +++ b/apps/fo/.env.local @@ -1,18 +1,19 @@ NEXT_PUBLIC_SITE_ID = FO NEXT_PUBLIC_PORT = 3000 -NEXT_PUBLIC_PROXY_HOST = http://localhost:3000 -NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 -#NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 -NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 -NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 -#NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver -NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver -NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token +NEXT_PUBLIC_DEV_CM_LOGIN_PAGE_URL = http://localhost:3001/auth/login +NEXT_PUBLIC_VDI_DEV_CM_LOGIN_PAGE_URL = http://localhost:3001/auth/login +NEXT_PUBLIC_VDI_DEV_CM_LOGIN_PAGE_URL = http://localhost:3001/auth/login +NEXT_PUBLIC_PROXY_HOST = http://localhost:3000 +NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082 +NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API - - +NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com/dc-service +REACT_EDITOR=atom \ No newline at end of file diff --git a/apps/fo/.env.mindone b/apps/fo/.env.mindone new file mode 100644 index 000000000..74d765a48 --- /dev/null +++ b/apps/fo/.env.mindone @@ -0,0 +1,14 @@ +NEXT_PUBLIC_SITE_ID = FO +NEXT_PUBLIC_PORT = 11300 +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token +NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11300 +NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 +NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g +NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api \ No newline at end of file diff --git a/apps/fo/.env.production b/apps/fo/.env.production index a1da344b8..3f5cf8bea 100644 --- a/apps/fo/.env.production +++ b/apps/fo/.env.production @@ -1,32 +1,17 @@ -# NEXT_PUBLIC_SITE_ID = FO -# NEXT_PUBLIC_PORT = 11300 -# NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11300 -# NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 -# #NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 -# NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 -# NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 -# NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver -# NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver -# NEXT_PUBLIC_CLAIM_NAME = Authorization -# NEXT_PUBLIC_AUTH_USER_ID = token-id -# NEXT_PUBLIC_ACCESS_TOKEN = access-token -# NEXT_PUBLIC_REFRESH_TOKEN = refresh-token -# NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API - +# 공단 운영 NEXT_PUBLIC_SITE_ID = FO NEXT_PUBLIC_PORT = 3000 -NEXT_PUBLIC_PROXY_HOST = https://fo.hasuon.or.kr -# NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000 -NEXT_PUBLIC_SERVER_API_URL = https://fo.hasuon.or.kr/operation-service/api/v1 -NEXT_PUBLIC_FO_SERVER_API_URL = https://apigateway.hasuon.or.kr/operation-service/api/v1 -NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000 -NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload -NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver -NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token -NEXT_PUBLIC_WR_SERVER_API_URL = https://fo.hasuon.or.kr/reuse-service/api/v1 - -NEXT_PUBLIC_SEWERAGE_API_URL = https://fo.hasuon.or.kr/sewerage-service/api \ No newline at end of file +NEXT_PUBLIC_PROXY_HOST = https://larva.hasuon.or.kr +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service +NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl +NEXT_PUBLIC_WR_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/reuse-service/api/v1 +NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/sewerage-service/api +NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/dc-service \ No newline at end of file diff --git a/apps/fo/app/base.tsx b/apps/fo/app/base.tsx deleted file mode 100644 index a01ce8643..000000000 --- a/apps/fo/app/base.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { BaseLayout } from "@common/components"; -import type { MenuItem } from "./store/common"; -import { useFoStore } from "./store"; -import { usePathname, useRouter } from "next/navigation"; - -export default function Base({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - const { getMenuAuth } = useFoStore((state) => state); - const pathname = usePathname(); - const router = useRouter(); - - const handleClick = (id: MenuItem) => { - getMenuAuth(id.menuCd); - router.push(id.urlPath); - }; - - return ( - - {children} - - ); -} diff --git a/apps/fo/app/components/api/config/env.ts b/apps/fo/app/components/api/config/env.ts index d419bfd1c..1c0ba8b0e 100644 --- a/apps/fo/app/components/api/config/env.ts +++ b/apps/fo/app/components/api/config/env.ts @@ -1,5 +1,5 @@ -import { PROXY_HOST, SERVER_API_URL, SERVER_API_WR_URL } from "@/src/shared/config/env"; +import { PROXY_HOST } from "@/src/shared/config/env"; -export const DEV_URL_FO = SERVER_API_URL; +export const DEV_URL_FO = "/operation-service/api/v1"; export const LOCAL_URL = PROXY_HOST; -export const DEV_URL_WR = SERVER_API_WR_URL; \ No newline at end of file +export const DEV_URL_WR = "/reuse-service/api/v1"; \ No newline at end of file diff --git a/apps/fo/app/components/api/index.ts b/apps/fo/app/components/api/index.ts index 3bba85af2..ba4e9863e 100644 --- a/apps/fo/app/components/api/index.ts +++ b/apps/fo/app/components/api/index.ts @@ -1,12 +1,22 @@ -import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "./config/env"; +import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config"; +import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "./config/env"; +import { getCookie } from "@common/components/lib/cookie"; + +const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || ""; + +const tokenId = getCookie(AUTH_USER_ID); +const Authorization = getCookie(CLAIM_NAME); export const getFetch = async (part : string, url: string) => { let reqUrl = ""; reqUrl = part === "fo" ? DEV_URL_FO+url : part === 'local' ? LOCAL_URL+url : DEV_URL_WR+url; + reqUrl = PROXY_HOST + reqUrl; const response = await fetch(reqUrl , { method: "GET", headers: { - accept: "application/json", + accept: "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", } }); if(!response.ok) { @@ -17,10 +27,12 @@ export const getFetch = async (part : string, url: string) => { } export const getFetchAreaList = async (url: string) => { - const response = await fetch(url , { + const response = await fetch(PROXY_HOST+url , { method: "GET", headers: { - accept: "application/json", + accept: "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", } }); if(!response.ok) { @@ -33,10 +45,13 @@ export const getFetchAreaList = async (url: string) => { export const postFetch = async (part : string, url: string, params: unknown) => { let reqUrl = ""; reqUrl = part === "fo" ? DEV_URL_FO+url : DEV_URL_WR+url; + reqUrl = PROXY_HOST + reqUrl; const response = await fetch(reqUrl, { method: "POST", headers: { "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); @@ -49,10 +64,13 @@ export const postFetch = async (part : string, url: string, params: unknown) => export const putFetch = async (part : string, url: string, params: unknown) => { let reqUrl = ""; reqUrl = part === "fo" ? DEV_URL_FO+url : DEV_URL_WR+url; + reqUrl = PROXY_HOST + reqUrl; const response = await fetch(reqUrl, { method: "PUT", headers: { - "Content-Type": "application/json", + "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); @@ -65,10 +83,13 @@ export const putFetch = async (part : string, url: string, params: unknown) => { export const deleteFetch = async (part : string, url: string, params: unknown) => { let reqUrl = ""; reqUrl = part === "fo" ? DEV_URL_FO+url : DEV_URL_WR+url; - const response = await fetch(reqUrl, { + reqUrl = PROXY_HOST + reqUrl; + const response = await fetch(reqUrl, { method: "DELETE", headers: { "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); diff --git a/apps/fo/app/layout.tsx b/apps/fo/app/layout.tsx index 842ec5ed1..683e6d0ec 100644 --- a/apps/fo/app/layout.tsx +++ b/apps/fo/app/layout.tsx @@ -1,29 +1,14 @@ -"use client" -import "./globals.css"; -import { StoreProvider } from "./store"; -import { LoginApi } from "@/src/entities/auth"; -import { useEffect } from "react"; +import type { Metadata } from "next"; +import { App } from "@/src/app"; -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { +export const metadata: Metadata = { + title: "디지털 하수관로", + description: "디지털 하수관로 시스템입니다", + keywords: ["하수관로"], + openGraph: { + title: "디지털 하수관로", + description: "디지털 하수관로 시스템입니다" + } +}; - useEffect(() => { - // 렌더 중이 아니라 클라이언트에서만 실행 + 에러 캐치 - void LoginApi.login({ userId: "CLEANWATER", password: "gktneh12!@" }) - .catch((e) => { - console.error("Auto login failed:", e); - }); - }, []); - return ( - - - - {children} - - - - ); -} +export default App; diff --git a/apps/fo/app/page.tsx b/apps/fo/app/page.tsx index 823e5a408..b1a103bca 100644 --- a/apps/fo/app/page.tsx +++ b/apps/fo/app/page.tsx @@ -1,44 +1,28 @@ -"use client" +"use client"; +import { LoginApi } from "@/src/entities/auth"; +import { useEffect } from "react"; +import { AUTH_USER_ID } from "@/src/shared/config"; +import { getCookie } from "@/src/shared/lib/cookie"; +import { useRouter } from "next/navigation"; -import Link from "next/link"; -import { Button } from "@common/components/ui"; export default function Home() { - return ( -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- ); -} + const router = useRouter(); + const checkToken = getCookie(AUTH_USER_ID); + + useEffect(() => { + if (!checkToken) { + async function doLogin() { + try { + await LoginApi.login({ userId: "CLEANWATER", password: "gktneh12!@" }); + router.push("/pages/dashboard"); + } catch (err) { + console.error("로그인 실패", err); + } + } + doLogin(); + } else { + router.push("/pages/dashboard"); + } + }, []); +} \ No newline at end of file diff --git a/apps/fo/app/pages/(with-layout)/layout.tsx b/apps/fo/app/pages/(with-layout)/layout.tsx index f7c0e62ba..851d79497 100644 --- a/apps/fo/app/pages/(with-layout)/layout.tsx +++ b/apps/fo/app/pages/(with-layout)/layout.tsx @@ -1,13 +1,30 @@ "use client" -import Base from "@/app/base"; +import { BaseLayout } from "@common/components"; +import type { MenuItem } from "@/app/store/common"; +import { useFoStore } from "@/app/store"; +import { usePathname, useRouter } from "next/navigation"; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const { getMenuAuth } = useFoStore((state) => state); + const pathname = usePathname(); + const router = useRouter(); + + const handleClick = (id: MenuItem) => { + getMenuAuth(id.menuCd); + router.push(id.urlPath); + }; + return ( - {children} + + {children} + ); } diff --git a/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx index fbda7a140..7e3f7c52f 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx @@ -117,7 +117,7 @@ export default function ProcessFacilityInfoModal(props: ModalType) { { value: 'common_info', label: '기본정보' }, { value: 'history_info', label: '이력정보' }, ]; - const fctCpc = props.fctCpc ? props.fctCpc.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : ''; + const fctCpc = props.fctCpc ? Number(props.fctCpc).toLocaleString() : ''; const upHeadListPlanInfoChange = headMakeList(upHeadListPlanInfo, 2); const upHeadListFcltyHistInfoChange = headMakeList(upHeadListFcltyHistInfo, 3); const upHeadListFcltyWaterQltyInfoChange = headMakeList(upHeadListFcltyWaterQltyInfo, 4); @@ -222,9 +222,9 @@ export default function ProcessFacilityInfoModal(props: ModalType) { {'시설명'} - {props.fcltyName} - {'시설구분'} - {props.bscFctClsNm} + {props.fcltyName} + {/* {'시설구분'} + {props.bscFctClsNm} */} {'시설용량(m³/일)'} {fctCpc} diff --git a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx index 0e9085cb4..775dd1a45 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx @@ -210,12 +210,13 @@ export default function ProcessFacilitySearch() { const cellClick = (index: number) => { const facility = processFacilityList[index]; if (facility) { + const fctCpc = Number(String(facility.fctCpc).replace(/,/g, "").trim()); setModalFcltyNm(facility.bscFctNm); setModalFcltyCd(facility.bscFctCd); setModalSido(facility.sido); setModalGugun(facility.gugun); setModalAddr(facility.daddr); - setModalFctCpc(Number(facility.fctCpc)); + setModalFctCpc(fctCpc); setModalTrtFrfNm(facility.trtFrfNm); setModalRnDt(facility.rnDt); setModalCmplDt(facility.cmplDt); diff --git a/apps/fo/app/store/common/index.tsx b/apps/fo/app/store/common/index.tsx index e75492d52..e4aebc982 100644 --- a/apps/fo/app/store/common/index.tsx +++ b/apps/fo/app/store/common/index.tsx @@ -232,7 +232,7 @@ export const COD_YEAR = 2021; */ // 처리시설 계획정보 호출 (알에스원) -export const planInfoUrl = 'https://apigateway.apps.rsonesoft.com/dc-service/api/v1/nowDmgMtncGnsvy'; +export const planInfoUrl = '/dc-service/api/v1/nowDmgMtncGnsvy'; export const fetchPlanInfo = async (targetYear : string | undefined, ctpvNm : string | undefined, sggNm : string | undefined, fcltyCd : string | undefined) => { try { diff --git a/apps/fo/middleware.ts b/apps/fo/middleware.ts index 32257341d..bb9c9effa 100644 --- a/apps/fo/middleware.ts +++ b/apps/fo/middleware.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { SERVER_API_CS_URL } from "./src/shared/config/env"; +import { SERVER_API_CS_URL, SERVER_API_FO_URL, SERVER_API_WR_URL } from "./src/shared/config/env"; const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; @@ -7,6 +7,8 @@ const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || ""; const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || ""; const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ""; const serverApiUrl = SERVER_API_CS_URL; +const DC_URL = process.env.NEXT_PUBLIC_DC_URL || ""; + export function middleware(req: NextRequest) { console.log("Middleware 진입:", req.nextUrl.pathname); const pathname = req.nextUrl.pathname; @@ -63,12 +65,12 @@ export function middleware(req: NextRequest) { return NextResponse.rewrite(new URL(newUrl)); } - if (pathname.startsWith("/sewerage-service/api/v1/map/geometry")) { + if (pathname.startsWith("/sewerage-service")) { const queryParams = searchParams.toString(); - const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service/api", "")}${ + const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service", "")}${ queryParams ? `?${queryParams}` : "" }`; - console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); + console.log(`[Middleware] Rewriting from '/sewerage-service' path to: ${newUrl}`); return NextResponse.rewrite(new URL(newUrl)); } @@ -89,40 +91,42 @@ export function middleware(req: NextRequest) { }); } - if (pathname.startsWith("/operation-service")) { - const requestHeaders = new Headers(req.headers); - const token = req.cookies.get(CLAIM_NAME)?.value; - const userId = req.cookies.get(AUTH_USER_ID)?.value; - const newUrl = `${serverApiUrl}${pathname}`; - - if (token) requestHeaders.set(CLAIM_NAME, token); - if (userId) requestHeaders.set(AUTH_USER_ID, userId); - - console.log( - `[Middleware] Rewriting from '/operation-service' path to: ${newUrl}` - ); - return NextResponse.rewrite(new URL(newUrl), { - headers: requestHeaders, - }); - } - if (pathname.startsWith("/reuse-service")) { + const queryParams = searchParams.toString(); + const newUrl = `${SERVER_API_WR_URL}${pathname.replace("/reuse-service", "")}${ + queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/reuse-service' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname.startsWith("/operation-service")) { + const queryParams = searchParams.toString(); + const newUrl = `${SERVER_API_FO_URL}${pathname.replace("/operation-service", "")}${ + queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/operation-service' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname.startsWith("/dc-service")) { const requestHeaders = new Headers(req.headers); + const queryParams = searchParams.toString(); + const newUrl = `${DC_URL}${pathname.replace("/dc-service", "")}${ + queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/dc-service' path to: ${newUrl}`); + const token = req.cookies.get(CLAIM_NAME)?.value; const userId = req.cookies.get(AUTH_USER_ID)?.value; - const newUrl = `${serverApiUrl}${pathname}`; - if (token) requestHeaders.set(CLAIM_NAME, token); if (userId) requestHeaders.set(AUTH_USER_ID, userId); - console.log( - `[Middleware] Rewriting from '/reuse-service' path to: ${newUrl}` - ); return NextResponse.rewrite(new URL(newUrl), { headers: requestHeaders, }); } - + if (pathname.startsWith("/user-service")) { const requestHeaders = new Headers(req.headers); const newUrl = `${SERVER_API_CS_URL}${pathname}`; @@ -135,21 +139,23 @@ export function middleware(req: NextRequest) { }); } - if (pathname === "/") { - return NextResponse.redirect(new URL("/pages/dashboard", req.url)); - } - return NextResponse.next(); + // if (pathname === "/") { + // return NextResponse.redirect(new URL("/pages/dashboard", req.url)); + // } + // return NextResponse.next(); } export const config = { matcher: [ - "/", - "/server/:path*", - "/geoserver/:path*", - "/attach-service/:path*", - "/user-service/:path*", - "/cms-service/:path*", - "/operation-service/:path*", - "/reuse-service/:path*", + "/", + "/server/:path*", + "/geoserver/:path*", + "/sewerage-service/:path*", + "/attach-service/:path*", + "/user-service/:path*", + "/cms-service/:path*", + "/operation-service/:path*", + "/reuse-service/:path*", + "/dc-service/:path*", ], }; diff --git a/apps/fo/package.json b/apps/fo/package.json index 8697a116a..53423decf 100644 --- a/apps/fo/package.json +++ b/apps/fo/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@common/assets": "workspace:*", "@common/business_components": "workspace:*", "@common/components": "workspace:*", "@tailwindcss/vite": "^4.0.10", diff --git a/apps/fo/src/app/index.tsx b/apps/fo/src/app/index.tsx new file mode 100644 index 000000000..692b55ef0 --- /dev/null +++ b/apps/fo/src/app/index.tsx @@ -0,0 +1,20 @@ +import "./styles/base.css"; +import "./styles/components.css"; +import "@fontsource-variable/noto-sans-kr"; + +import { AppProvider } from "./providers"; +import { cookies } from "next/headers"; + +const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => { + const theme = (await cookies()).get("site-theme-key"); + + return ( + + + {children} + + + ); +}; + +export { App }; diff --git a/apps/fo/src/app/providers/app-provider.tsx b/apps/fo/src/app/providers/app-provider.tsx new file mode 100644 index 000000000..edce6071e --- /dev/null +++ b/apps/fo/src/app/providers/app-provider.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { DndProvider } from "react-dnd"; +import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { ConfirmProvider } from "@frontend-opensource/use-react-hooks"; +import { AuthWrapper } from "@/src/entities/auth/lib/auth-wrapper"; +import { MenuWrapper } from "@/src/entities/menu"; +import { PromptProvider, StoreProvider } from "@/src/shared/providers"; +import { AlertDialog } from "@/src/shared/ui/alert-dialog"; +import { PromptDialog } from "@/src/shared/ui/prompt-dialog"; +import { ConfirmDialog } from "@/src/shared/ui/confirm-dialog"; +import { StoreProvider as FoStoreProvider } from "@/app/store"; + + +const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { + const queryClient = new QueryClient(); + return ( + + + + + + + + + {children} + + + + + + + + + + + +
+ +
+
+ ); +}; + +export { AppProvider }; diff --git a/apps/fo/src/app/providers/index.ts b/apps/fo/src/app/providers/index.ts new file mode 100644 index 000000000..15f99b7fd --- /dev/null +++ b/apps/fo/src/app/providers/index.ts @@ -0,0 +1 @@ +export { AppProvider } from "./app-provider"; diff --git a/apps/fo/src/app/styles/base.css b/apps/fo/src/app/styles/base.css new file mode 100644 index 000000000..8453ff9e8 --- /dev/null +++ b/apps/fo/src/app/styles/base.css @@ -0,0 +1,12 @@ +@import "@common/assets/styles/base.css"; +@import "@common/assets/styles/diveicon.css"; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body { + font-family: "Noto Sans KR", sans-serif; + font-size: 10px; +} diff --git a/apps/fo/src/app/styles/components.css b/apps/fo/src/app/styles/components.css new file mode 100644 index 000000000..4dc30bafd --- /dev/null +++ b/apps/fo/src/app/styles/components.css @@ -0,0 +1,29 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="black"] { + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/fo/src/entities/auth/lib/auth-wrapper.tsx b/apps/fo/src/entities/auth/lib/auth-wrapper.tsx index 678ff50d5..8f77aa8af 100644 --- a/apps/fo/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/fo/src/entities/auth/lib/auth-wrapper.tsx @@ -1,45 +1,54 @@ -// "use client"; +"use client"; -// import { useEffect } from "react"; -// import { useRouter } from "next/navigation"; -// import { LoginApi } from "../api/login-service"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { client } from "@/src/shared/api"; +import type { IUserInfo } from "@/src/shared/store"; +import { useUserStore, getCookie } from "@/src/shared/lib"; +import { ENDPOINT, AUTH_USER_ID } from "@/src/shared/config"; +import { LoginApi } from "../api/login-service"; -// const AuthWrapper = ({ children }: { children: React.ReactNode }) => { -// const router = useRouter(); -// const { userInfo, setUserInfo } = useUserStore((state) => state); +//const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_DEV_CM_LOGIN_PAGE_URL || "http://localhost:3001/auth/login"; // 개발 환경 URL -// async function authCheck() { -// const userId = getCookie(AUTH_USER_ID); -// if (!userId) { -// throw new Error("Missing required authentication tokens"); -// } +const AuthWrapper = ({ children }: { children: React.ReactNode }) => { + const router = useRouter(); + const { userInfo, setUserInfo } = useUserStore((state) => state); -// const response = await client.get( -// `${ENDPOINT.USER_SERVICE.USERS}/info/me`, -// ); -// if (!userInfo) setUserInfo(response.data); -// } + async function authCheck() { + const userId = getCookie(AUTH_USER_ID); + if (!userId) { + throw new Error("Missing required authentication tokens"); + } -// useEffect(() => { -// (async () => { -// try { -// await authCheck(); -// } catch (error) { -// console.error(error); + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/info/me`, + ); + if (!userInfo) setUserInfo(response.data); + } -// try { -// await LoginApi.silentRefresh(); -// await authCheck(); -// } catch (refreshError) { -// console.error("Token refresh failed:", refreshError); -// setUserInfo(null); -// router.push("/auth/login"); -// } -// } -// })(); -// }, []); + useEffect(() => { + (async () => { + try { + await authCheck(); + } catch (error) { + console.error(error); -// return <>{children}; -// }; + try { + await LoginApi.silentRefresh(); + await authCheck(); + } catch (refreshError) { + console.error("Token refresh failed:", refreshError); + setUserInfo(null); + router.push("/"); // 개발시 홈으로 돌아가기 -// export { AuthWrapper }; + // window.location.href = LOGIN_PAGE_URL; // 세션 만료 시 CM 로그인 페이지로 리다이렉트 + // router.push는 같은 Next.js 앱 내에서만 작동하므로 다른 앱으로 이동할 때는 window.location.href 사용 + } + } + })(); + }, []); + + return <>{children}; +}; + +export { AuthWrapper }; diff --git a/apps/fo/src/entities/menu-authority/@x/menu.ts b/apps/fo/src/entities/menu-authority/@x/menu.ts new file mode 100644 index 000000000..86479f859 --- /dev/null +++ b/apps/fo/src/entities/menu-authority/@x/menu.ts @@ -0,0 +1 @@ +export { MenuAuthorityApi } from "../api/menu-authority-service"; diff --git a/apps/fo/src/entities/menu-authority/@x/posting.ts b/apps/fo/src/entities/menu-authority/@x/posting.ts new file mode 100644 index 000000000..bfe84fa0d --- /dev/null +++ b/apps/fo/src/entities/menu-authority/@x/posting.ts @@ -0,0 +1 @@ +export { AuthorityButton } from "../ui/authority-button"; diff --git a/apps/fo/src/entities/menu-authority/api/menu-authority-service.ts b/apps/fo/src/entities/menu-authority/api/menu-authority-service.ts new file mode 100644 index 000000000..c7ac056f4 --- /dev/null +++ b/apps/fo/src/entities/menu-authority/api/menu-authority-service.ts @@ -0,0 +1,40 @@ +import { client } from "@/src/shared/api"; +import { ENDPOINT } from "@/src/shared/config"; +import type { + MenuAuthorityList, + MenuAuthorityParams, + MenuAuthorityPayload, + MenuAuthorityService, + PageMenuAuthority, +} from "../model/menu-authority-interface"; + +export class MenuAuthorityApiService implements MenuAuthorityService { + public async getMenuAuthorityList( + params: MenuAuthorityParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`, + { params }, + ); + return response.data; + } + + public async getPageMenuAuthority( + menuCd: number, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}/pageinfo/${menuCd}`, + ); + return response.data; + } + + public async saveMenuAuthority(data: MenuAuthorityPayload[]): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`, + data, + ); + return response.data; + } +} + +export const MenuAuthorityApi = new MenuAuthorityApiService(); diff --git a/apps/fo/src/entities/menu-authority/index.ts b/apps/fo/src/entities/menu-authority/index.ts new file mode 100644 index 000000000..2a555b77e --- /dev/null +++ b/apps/fo/src/entities/menu-authority/index.ts @@ -0,0 +1,2 @@ +export { AuthorityButton } from "./ui/authority-button"; +export { ManagementAuthorityDialog } from "./ui/management-authority-dialog"; diff --git a/apps/fo/src/entities/menu-authority/model/menu-authority-interface.ts b/apps/fo/src/entities/menu-authority/model/menu-authority-interface.ts new file mode 100644 index 000000000..f659f58af --- /dev/null +++ b/apps/fo/src/entities/menu-authority/model/menu-authority-interface.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/src/shared/model"; + +export interface MenuAuthorityParams extends PageableParams { + groupId?: string; + menuCd?: number; + useYn?: string; +} + +export interface PageMenuAuthority { + mngAuthrtYn: string; + inqAuthrtYn: string; + regAuthrtYn: string; + mdfcnAuthrtYn: string; + delAuthrtYn: string; +} + +export const menuAuthoritySchema = z.object({ + groupId: z.string(), + groupNm: z.string(), + menuCd: z.number(), + mngAuthrtYn: z.string(), + inqAuthrtYn: z.string(), + regAuthrtYn: z.string(), + mdfcnAuthrtYn: z.string(), + delAuthrtYn: z.string(), + useYn: z.string(), + mode: z.string(), +}); + +export const menuAuthorityPayloadSchema = menuAuthoritySchema.omit({ + groupNm: true, +}); + +export const menuAuthorityFormSchema = z.object({ + menuAuthority: z.array(menuAuthorityPayloadSchema), +}); + +export type MenuAuthority = z.infer; +export type MenuAuthorityList = PageableList; +export type MenuAuthorityPayload = z.infer; +export type MenuAuthorityFormData = z.infer; + +export interface MenuAuthorityService { + getMenuAuthorityList(params: MenuAuthorityParams): Promise; + getPageMenuAuthority(menuCd: number): Promise; + saveMenuAuthority(data: MenuAuthorityPayload[]): Promise; +} diff --git a/apps/fo/src/entities/menu-authority/model/menu-authority-table-columns.tsx b/apps/fo/src/entities/menu-authority/model/menu-authority-table-columns.tsx new file mode 100644 index 000000000..6969a85e9 --- /dev/null +++ b/apps/fo/src/entities/menu-authority/model/menu-authority-table-columns.tsx @@ -0,0 +1,117 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import type { UseFormReturn } from "react-hook-form"; +import { Checkbox } from "@common/components"; +import type { + MenuAuthority, + MenuAuthorityFormData, + MenuAuthorityPayload, +} from "./menu-authority-interface"; + +export const createColumns = ( + form: UseFormReturn, +): ColumnDef[] => { + const getCurrentRow = (i: number) => form.watch("menuAuthority")[i]; + + const handleCheckedChange = + (i: number, key: keyof MenuAuthorityPayload) => (checked: boolean) => { + if (getCurrentRow(i)?.mode === "R") { + form.setValue(`menuAuthority.${i}.mode`, "U"); + } + form.setValue(`menuAuthority.${i}.${key}`, checked ? "Y" : "N"); + }; + + return [ + { + accessorKey: "groupNm", + header: "그룹명", + }, + { + accessorKey: "mngAuthrtYn", + header: "관리권한", + cell: ({ row }) => { + return ( + { + if (checked) { + handleCheckedChange( + row.index, + "regAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "mdfcnAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "delAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "inqAuthrtYn", + )(checked as boolean); + } + handleCheckedChange(row.index, "mngAuthrtYn")(checked as boolean); + }} + /> + ); + }, + }, + { + accessorKey: "regAuthrtYn", + header: "등록권한", + cell: ({ row }) => { + return ( + { + if (checked) { + handleCheckedChange( + row.index, + "inqAuthrtYn", + )(checked as boolean); + } + handleCheckedChange(row.index, "regAuthrtYn")(checked as boolean); + }} + /> + ); + }, + }, + { + accessorKey: "mdfcnAuthrtYn", + header: "수정권한 ", + cell: ({ row }) => { + return ( + + ); + }, + }, + { + accessorKey: "delAuthrtYn", + header: "삭제권한", + cell: ({ row }) => { + return ( + + ); + }, + }, + { + accessorKey: "inqAuthrtYn", + header: "조회권한", + cell: ({ row }) => { + return ( + + ); + }, + }, + ]; +}; diff --git a/apps/fo/src/entities/menu-authority/ui/authority-button.tsx b/apps/fo/src/entities/menu-authority/ui/authority-button.tsx new file mode 100644 index 000000000..a4cd666d9 --- /dev/null +++ b/apps/fo/src/entities/menu-authority/ui/authority-button.tsx @@ -0,0 +1,26 @@ +import { useMenuStore } from "@/src/shared/lib"; +import { Button, cn, type ButtonProps } from "@common/components"; +import type { PageMenuAuthority } from "../model/menu-authority-interface"; + +type AuthorityButtonProps = { + authority: keyof PageMenuAuthority; +} & ButtonProps; + +function AuthorityButton({ + authority, + className, + children, + ...props +}: AuthorityButtonProps) { + const pageMenuAuthority = useMenuStore((state) => state.pageMenuAuthority); + + const isAuthority = pageMenuAuthority?.[authority] === "Y"; + + return ( + + ); +} + +export { AuthorityButton }; diff --git a/apps/fo/src/entities/menu-authority/ui/management-authority-dialog.tsx b/apps/fo/src/entities/menu-authority/ui/management-authority-dialog.tsx new file mode 100644 index 000000000..839b9a7a4 --- /dev/null +++ b/apps/fo/src/entities/menu-authority/ui/management-authority-dialog.tsx @@ -0,0 +1,103 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; +import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + DataTable, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@common/components"; +import { ENDPOINT } from "@/src/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/src/shared/lib"; +import { + menuAuthorityFormSchema, + type MenuAuthorityFormData, + type MenuAuthorityPayload, +} from "../model/menu-authority-interface"; +import { createColumns } from "../model/menu-authority-table-columns"; +import { MenuAuthorityApi } from "../api/menu-authority-service"; + +type ManagementAuthorityDialogProps = { + menuCd?: number; +} & React.ComponentProps; + +function ManagementAuthorityDialog({ + menuCd, + ...props +}: ManagementAuthorityDialogProps) { + const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); + + const { data: menuAuthorityList } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, menuCd], + queryFn: () => MenuAuthorityApi.getMenuAuthorityList({ menuCd }), + enabled: !!menuCd, + }); + + const { mutateAsync: saveMenuAuthority } = useMutationWrapper({ + mutationFn: (data: MenuAuthorityPayload[]) => + MenuAuthorityApi.saveMenuAuthority(data), + onSuccess: () => { + alert("저장되었습니다."); + }, + }); + + const form = useForm({ + resolver: zodResolver(menuAuthorityFormSchema), + values: { + menuAuthority: menuAuthorityList?.content ?? [], + }, + }); + + const table = useReactTable({ + data: menuAuthorityList?.content ?? [], + columns: createColumns(form), + getCoreRowModel: getCoreRowModel(), + }); + + const handleSave = async () => { + if (await confirm("저장하시겠습니까?")) { + const data = form.getValues("menuAuthority"); + const filteredData = data.filter((item) => item.mode !== "R"); + await saveMenuAuthority(filteredData); + } + }; + + return ( + + + + + + + 그룹별 메뉴 권한 + +
+ +
+ + + + + + +
+
+ ); +} + +export { ManagementAuthorityDialog }; diff --git a/apps/fo/src/entities/menu/api/menu-service.ts b/apps/fo/src/entities/menu/api/menu-service.ts new file mode 100644 index 000000000..8b08d7fa5 --- /dev/null +++ b/apps/fo/src/entities/menu/api/menu-service.ts @@ -0,0 +1,14 @@ +import { client } from "@/src/shared/api"; +import { ENDPOINT } from "@/src/shared/config"; +import type { MenuItem, MenuService } from "../model/menu-interface"; + +export class MenuApiService implements MenuService { + async getMenuRoles(siteId: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`, + ); + return response.data; + } +} + +export const MenuApi = new MenuApiService(); diff --git a/apps/fo/src/entities/menu/index.ts b/apps/fo/src/entities/menu/index.ts new file mode 100644 index 000000000..04f63e38b --- /dev/null +++ b/apps/fo/src/entities/menu/index.ts @@ -0,0 +1,2 @@ +export { MenuWrapper } from "./lib/menu-wrapper"; +export type { MenuItem } from "./model/menu-interface"; diff --git a/apps/fo/src/entities/menu/lib/menu-wrapper.tsx b/apps/fo/src/entities/menu/lib/menu-wrapper.tsx new file mode 100644 index 000000000..f445123d3 --- /dev/null +++ b/apps/fo/src/entities/menu/lib/menu-wrapper.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useEffect } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { MenuAuthorityApi } from "@/src/entities/menu-authority/@x/menu"; +import { useAlertStore, useMenuStore } from "@/src/shared/lib"; +import { getCookie } from "@/src/shared/lib";; +import { SITE_ID } from "@/src/shared/config"; +import { MenuApi } from "../api/menu-service"; +import { getCurrentMenu } from "./utils"; + +const MenuWrapper = ({ children }: { children: React.ReactNode }) => { + const router = useRouter(); + const pathname = usePathname(); + + const alert = useAlertStore((state) => state.setMessage); + const { pageMenuAuthority, setCurrentMenuInfo, setPageMenuAuthority } = + useMenuStore((state) => state); + + const getCurrentMenuInfo = async () => { + const menuRoles = await MenuApi.getMenuRoles(SITE_ID); + if (!menuRoles) return; + return getCurrentMenu(menuRoles, pathname ?? ""); + }; + + const getPageMenuAuthority = async () => { + const menuCd = getCookie("menu-code-key"); + if (!menuCd) return; + const response = await MenuAuthorityApi.getPageMenuAuthority( + parseInt(menuCd), + ); + return response; + }; + + const checkInquiryAuthority = () => { + return pageMenuAuthority?.inqAuthrtYn === "Y"; + }; + + useEffect(() => { + (async () => { + const currentMenu = await getCurrentMenuInfo(); + if (currentMenu) setCurrentMenuInfo(currentMenu); + + const response = await getPageMenuAuthority(); + if (response?.length) setPageMenuAuthority(response[0]!); + })(); + }, []); + + useEffect(() => { + if (!pageMenuAuthority) return; + const inquiryAuthority = checkInquiryAuthority(); + if (!inquiryAuthority) + alert("페이지에 대한 조회 권한이 없습니다.", () => router.push("/")); + }, [pageMenuAuthority]); + + return <>{children}; +}; + +export { MenuWrapper }; diff --git a/apps/fo/src/entities/menu/lib/utils.ts b/apps/fo/src/entities/menu/lib/utils.ts new file mode 100644 index 000000000..1cf4514d9 --- /dev/null +++ b/apps/fo/src/entities/menu/lib/utils.ts @@ -0,0 +1,20 @@ +import type { MenuItem } from "../model/menu-interface"; + +export const getCurrentMenu = ( + menus: MenuItem[], + pathname: string, +): MenuItem | null => { + for (const menu of menus) { + if ( + menu.urlPath !== "" && + (menu.urlPath === pathname || pathname.startsWith(menu.urlPath)) + ) + return menu; + + if (menu.children) { + const children = getCurrentMenu(menu.children, pathname); + if (children) return children; + } + } + return null; +}; diff --git a/apps/fo/src/entities/menu/model/menu-interface.ts b/apps/fo/src/entities/menu/model/menu-interface.ts new file mode 100644 index 000000000..12c7d007a --- /dev/null +++ b/apps/fo/src/entities/menu/model/menu-interface.ts @@ -0,0 +1,23 @@ +export interface MenuItem { + menuCd: number; + menuKornNm: string; + menuEngNm: string; + parentId: number | null; + sortSeq: number; + iconNm?: string; + children: MenuItem[] | null; + levelNo: number; + urlPath: string; + menuLnkgTypeCd: string; + menuLnkgSn: number; + menuPath: string; + qrcd: string; + useYn: string; + otptYn: string; + newWndwYn: string; + requiredUrlPath: boolean; +} + +export interface MenuService { + getMenuRoles(siteId: string): Promise; +} diff --git a/apps/fo/src/shared/api/client.ts b/apps/fo/src/shared/api/client.ts new file mode 100644 index 000000000..cf37e6671 --- /dev/null +++ b/apps/fo/src/shared/api/client.ts @@ -0,0 +1,11 @@ +import { Client } from "@common/network"; +import { BASE_URL } from "../config/api"; +import { SITE_ID } from "../config/env"; +import { setCookie } from "../lib"; + +setCookie("site-id-key", SITE_ID); + +export const client = Client.getInstance({ + baseURL: BASE_URL, + siteId: SITE_ID, +}); diff --git a/apps/fo/src/shared/api/index.ts b/apps/fo/src/shared/api/index.ts new file mode 100644 index 000000000..b7b9d6121 --- /dev/null +++ b/apps/fo/src/shared/api/index.ts @@ -0,0 +1 @@ +export { client } from "./client"; diff --git a/apps/fo/src/shared/config/env.ts b/apps/fo/src/shared/config/env.ts index 8baf1b0f0..6e3bf223e 100644 --- a/apps/fo/src/shared/config/env.ts +++ b/apps/fo/src/shared/config/env.ts @@ -2,7 +2,7 @@ export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "FO"; export const PORT = process.env.NEXT_PUBLIC_PORT || "3000"; export const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`; export const SERVER_API_WR_URL = process.env.NEXT_PUBLIC_WR_SERVER_API_URL ||'http://con.mind-one.co.kr:11080/api/v1'; -export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL||' http://con.mind-one.co.kr:11082/api/v1'; +export const SERVER_API_FO_URL = process.env.NEXT_PUBLIC_SERVER_API_URL||' http://con.mind-one.co.kr:11082/api/v1'; export const SERVER_API_CS_URL = process.env.NEXT_PUBLIC_CS_SERVER_API_URL||'http://dev.onasys.co.kr:8000'; export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; export const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; diff --git a/apps/fo/src/shared/lib/index.ts b/apps/fo/src/shared/lib/index.ts new file mode 100644 index 000000000..b8b4c52c0 --- /dev/null +++ b/apps/fo/src/shared/lib/index.ts @@ -0,0 +1,6 @@ +export { useMutationWrapper } from "./use-mutation-wrapper"; +export { useAlertStore } from "./use-alert-store"; +export { useMenuStore } from "./use-menu-store"; +export { useUserStore } from "./use-user-store"; +export { usePrompt } from "./use-prompt"; +export { getCookie, setCookie, removeCookie } from "./cookie"; diff --git a/apps/fo/src/shared/lib/use-alert-store.ts b/apps/fo/src/shared/lib/use-alert-store.ts new file mode 100644 index 000000000..c21a00ed3 --- /dev/null +++ b/apps/fo/src/shared/lib/use-alert-store.ts @@ -0,0 +1,14 @@ +import { useStore } from "zustand"; +import { useContext } from "react"; +import type { AlertStore } from "../store/alert-store"; +import { StoreContext } from "../providers/store-provider"; + +export const useAlertStore = (selector: (store: AlertStore) => T): T => { + const storeContext = useContext(StoreContext); + + if (!storeContext) { + throw new Error("useAlertStore must be used within StoreProvider"); + } + + return useStore(storeContext.alertStore, selector); +}; diff --git a/apps/fo/src/shared/lib/use-menu-store.ts b/apps/fo/src/shared/lib/use-menu-store.ts new file mode 100644 index 000000000..66e41c76c --- /dev/null +++ b/apps/fo/src/shared/lib/use-menu-store.ts @@ -0,0 +1,14 @@ +import { useStore } from "zustand"; +import { useContext } from "react"; +import type { MenuStore } from "../store/menu-store"; +import { StoreContext } from "../providers/store-provider"; + +export const useMenuStore = (selector: (store: MenuStore) => T): T => { + const storeContext = useContext(StoreContext); + + if (!storeContext) { + throw new Error("useMenuStore must be used within StoreProvider"); + } + + return useStore(storeContext.menuStore, selector); +}; diff --git a/apps/fo/src/shared/lib/use-mutation-wrapper.tsx b/apps/fo/src/shared/lib/use-mutation-wrapper.tsx new file mode 100644 index 000000000..ce119d2a7 --- /dev/null +++ b/apps/fo/src/shared/lib/use-mutation-wrapper.tsx @@ -0,0 +1,31 @@ +import { AxiosError } from "axios"; +import { useMutation, type UseMutationOptions } from "@tanstack/react-query"; +import { useAlertStore } from "./use-alert-store"; + +export const useMutationWrapper = < + TData = unknown, + TError = unknown, + TVariables = void, + TContext = unknown, +>( + mutationOptions: UseMutationOptions, +) => { + const alert = useAlertStore((state) => state.setMessage); + return useMutation({ + ...mutationOptions, + onError: (error) => { + let errorMessage = ""; + + if (error instanceof AxiosError) { + if (error.response?.status) + errorMessage += `status: ${error.response.status}\n`; + if (error.response?.data?.message) + errorMessage += `${error.response.data.message}\n`; + if (error.response?.data?.errors[0]?.message) + errorMessage += `${error.response.data.errors[0].message}\n`; + } + + alert(`[Error] ${errorMessage}`); + }, + }); +}; diff --git a/apps/fo/src/shared/lib/use-prompt.tsx b/apps/fo/src/shared/lib/use-prompt.tsx new file mode 100644 index 000000000..f36858eb0 --- /dev/null +++ b/apps/fo/src/shared/lib/use-prompt.tsx @@ -0,0 +1,26 @@ +import { useContext } from "react"; +import { PromptContext } from "../providers/prompt-provider"; + +export const usePrompt = () => { + const { message, resolve, setMessage, setResolve } = + useContext(PromptContext); + + const prompt = (message: string) => { + setMessage(message); + return new Promise<{ resolved: boolean; value?: string }>((resolve) => { + setResolve(() => resolve); + }); + }; + + const onSubmit = (value: string) => { + resolve?.({ resolved: true, value }); + setMessage(""); + }; + + const onCancel = () => { + resolve?.({ resolved: false }); + setMessage(""); + }; + + return { message, isOpen: !!message, prompt, onSubmit, onCancel }; +}; diff --git a/apps/fo/src/shared/lib/use-user-store.ts b/apps/fo/src/shared/lib/use-user-store.ts new file mode 100644 index 000000000..748caed4c --- /dev/null +++ b/apps/fo/src/shared/lib/use-user-store.ts @@ -0,0 +1,14 @@ +import { useStore } from "zustand"; +import { useContext } from "react"; +import type { UserStore } from "../store/user-store"; +import { StoreContext } from "../providers/store-provider"; + +export const useUserStore = (selector: (store: UserStore) => T): T => { + const storeContext = useContext(StoreContext); + + if (!storeContext) { + throw new Error("useUserStore must be used within StoreProvider"); + } + + return useStore(storeContext.userStore, selector); +}; diff --git a/apps/fo/src/shared/providers/index.ts b/apps/fo/src/shared/providers/index.ts new file mode 100644 index 000000000..fa8320fe7 --- /dev/null +++ b/apps/fo/src/shared/providers/index.ts @@ -0,0 +1,2 @@ +export { StoreProvider } from "./store-provider"; +export { PromptProvider } from "./prompt-provider"; diff --git a/apps/fo/src/shared/providers/prompt-provider.tsx b/apps/fo/src/shared/providers/prompt-provider.tsx new file mode 100644 index 000000000..10338c338 --- /dev/null +++ b/apps/fo/src/shared/providers/prompt-provider.tsx @@ -0,0 +1,37 @@ +import { createContext, useState } from "react"; + +interface PromptContext { + message?: string; + resolve?: ({ + resolved, + value, + }: { + resolved: boolean; + value?: string; + }) => void; + setMessage: (message: string) => void; + setResolve: (resolve: () => void) => void; +} + +export const PromptContext = createContext({ + message: "", + resolve() {}, + setMessage() {}, + setResolve() {}, +}); + +export const PromptProvider = ({ children }: { children: React.ReactNode }) => { + const [message, setMessage] = useState(); + const [resolve, setResolve] = + useState< + ({ resolved, value }: { resolved: boolean; value?: string }) => void + >(); + + return ( + + {children} + + ); +}; diff --git a/apps/fo/src/shared/providers/store-provider.tsx b/apps/fo/src/shared/providers/store-provider.tsx new file mode 100644 index 000000000..a0ec17b61 --- /dev/null +++ b/apps/fo/src/shared/providers/store-provider.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { createContext, useRef } from "react"; +import { + createAlertStore, + createAttachmentStore, + createMenuStore, + createUserStore, +} from "../store"; + +type StoreContextType = { + alertStore: ReturnType; + attachmentStore: ReturnType; + menuStore: ReturnType; + userStore: ReturnType; +}; + +export const StoreContext = createContext( + undefined, +); + +export const StoreProvider = ({ children }: { children: React.ReactNode }) => { + const storeRef = useRef(null); + if (storeRef.current === null) { + storeRef.current = { + alertStore: createAlertStore(), + attachmentStore: createAttachmentStore(), + menuStore: createMenuStore(), + userStore: createUserStore(), + }; + } + + return ( + + {children} + + ); +}; diff --git a/apps/fo/src/shared/store/alert-store.ts b/apps/fo/src/shared/store/alert-store.ts new file mode 100644 index 000000000..3bba65848 --- /dev/null +++ b/apps/fo/src/shared/store/alert-store.ts @@ -0,0 +1,25 @@ +import { createStore } from "zustand/vanilla"; + +export type AlertState = { + message: string; + callback?: () => void; +}; + +export type AlertActions = { + setMessage: (message: string, callback?: () => void) => void; +}; + +export type AlertStore = AlertState & AlertActions; + +export const defaultInitState: AlertState = { + message: "", + callback: undefined, +}; + +export const createAlertStore = (initState: AlertState = defaultInitState) => { + return createStore()((set) => ({ + ...initState, + setMessage: (message: string, callback?: () => void) => + set(() => ({ message, callback })), + })); +}; diff --git a/apps/fo/src/shared/store/attachment-store.ts b/apps/fo/src/shared/store/attachment-store.ts new file mode 100644 index 000000000..faa700949 --- /dev/null +++ b/apps/fo/src/shared/store/attachment-store.ts @@ -0,0 +1,25 @@ +import { createStore } from "zustand/vanilla"; + +export type AttachmentState = { + attachmentFileCode: string; +}; + +export type AttachmentActions = { + setAttachmentFileCode: (attachmentFileCode: string) => void; +}; + +export type AttachmentStore = AttachmentState & AttachmentActions; + +export const defaultInitState: AttachmentState = { + attachmentFileCode: "", +}; + +export const createAttachmentStore = ( + initState: AttachmentState = defaultInitState, +) => { + return createStore()((set) => ({ + ...initState, + setAttachmentFileCode: (attachmentFileCode: string) => + set({ attachmentFileCode }), + })); +}; diff --git a/apps/fo/src/shared/store/index.ts b/apps/fo/src/shared/store/index.ts new file mode 100644 index 000000000..b0ca59c6c --- /dev/null +++ b/apps/fo/src/shared/store/index.ts @@ -0,0 +1,4 @@ +export { createAlertStore } from "./alert-store"; +export { createAttachmentStore } from "./attachment-store"; +export { createMenuStore, type BbsType } from "./menu-store"; +export { createUserStore, type IUserInfo } from "./user-store"; \ No newline at end of file diff --git a/apps/fo/src/shared/store/menu-store.ts b/apps/fo/src/shared/store/menu-store.ts new file mode 100644 index 000000000..8536011ee --- /dev/null +++ b/apps/fo/src/shared/store/menu-store.ts @@ -0,0 +1,93 @@ +import { createStore } from "zustand/vanilla"; + +export const BBS_TYPE = { + BBSNOTICE: "BBSNOTICE", + BBSNORMAL: "BBSNORMAL", + BBSFAQ: "BBSFAQ", + BBSIMAGE: "BBSIMAGE", + BBSMOVIE: "BBSMOVIE", +} as const; + +export type BbsType = (typeof BBS_TYPE)[keyof typeof BBS_TYPE]; + +export interface IBbs { + bbsId: number; + bbsTypeCd: BbsType; + bbsTypeNm: string; + bbsNm: string; + bbsExpln: string; + rfrncUseYn: string; + atflUseYn: string; + atflMaxCnt: number; + atflMaxFileSz: number; + ansUseYn: string; + ansUseAuthrt: string; + cmntUseYn: string; + dgstfnEvlUseYn: string; + smsUseYn: string; + upndFixUseYn: string; + pstgPrdUseYn: string; + popupUseYn: string; + delYn: string; + useYn: string; + mdfcnDt: string; + mdfrNm: string; +} + +interface IMenu { + menuCd: number; + menuKornNm: string; + menuEngNm: string; + parentId: number | null; + sortSeq: number; + iconNm?: string; + children: IMenu[] | null; + levelNo: number; + urlPath: string; + menuLnkgTypeCd: string; + menuLnkgSn: number; + menuPath: string; + qrcd: string; + useYn: string; + otptYn: string; + newWndwYn: string; + requiredUrlPath: boolean; +} + +interface IAuthority { + mngAuthrtYn: string; + inqAuthrtYn: string; + regAuthrtYn: string; + mdfcnAuthrtYn: string; + delAuthrtYn: string; +} + +export type MenuState = { + bbsInfo: IBbs | null; + currentMenuInfo: IMenu | null; + pageMenuAuthority: IAuthority | null; +}; + +export type MenuActions = { + setBbsInfo: (bbs: IBbs) => void; + setCurrentMenuInfo: (menu: IMenu) => void; + setPageMenuAuthority: (authority: IAuthority) => void; +}; + +export type MenuStore = MenuState & MenuActions; + +export const defaultInitState: MenuState = { + currentMenuInfo: null, + bbsInfo: null, + pageMenuAuthority: null, +}; + +export const createMenuStore = (initState: MenuState = defaultInitState) => { + return createStore()((set) => ({ + ...initState, + setBbsInfo: (bbs: IBbs) => set({ bbsInfo: bbs }), + setCurrentMenuInfo: (menu: IMenu) => set({ currentMenuInfo: menu }), + setPageMenuAuthority: (authority: IAuthority) => + set({ pageMenuAuthority: authority }), + })); +}; diff --git a/apps/fo/src/shared/store/user-store.ts b/apps/fo/src/shared/store/user-store.ts new file mode 100644 index 000000000..65d897cf1 --- /dev/null +++ b/apps/fo/src/shared/store/user-store.ts @@ -0,0 +1,57 @@ +import { createStore } from "zustand/vanilla"; + +export interface IUserInfo { + regDt: string; + mdfcnDt: string; + rgtr: string; + mdfr: string; + adminRoleYn: string; + userUnqId: string; + userId: string; + userName: string; + email: string; + encryptedPassword: string; + refreshToken: string; + userStateCode: string; + lastLoginDate: string; + loginFailCount: number; + pswdHnt: string; + pswdAns: string; + mphn: string; + telno: string; + zip: string; + addr: string; + daddr: string; + laeSeCd: string; + ogdpSeCd: string; + aprvYn: string; + autzrId: string; + expryYmd: string; + lastPswdChgDt: string; + googleId: string; + kakaoId: string; + naverId: string; + isSocialUser: boolean; + hasPassword: boolean; +} + +export type UserState = { + userInfo: IUserInfo | null; +}; + +export type UserActions = { + setUserInfo: (userInfo: IUserInfo | null) => void; +}; + +export type UserStore = UserState & UserActions; + +export const defaultInitState: UserState = { + userInfo: null, +}; + +export const createUserStore = (initState: UserState = defaultInitState) => { + return createStore()((set) => ({ + ...initState, + setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })), + })); +}; diff --git a/apps/fo/src/shared/ui/alert-dialog.tsx b/apps/fo/src/shared/ui/alert-dialog.tsx new file mode 100644 index 000000000..6160d5301 --- /dev/null +++ b/apps/fo/src/shared/ui/alert-dialog.tsx @@ -0,0 +1,34 @@ +import { + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Button, +} from "@common/components"; +import { useAlertStore } from "../lib/use-alert-store"; + +function AlertDialog() { + const { message, callback, setMessage } = useAlertStore((state) => state); + const handleOpenChange = () => { + setMessage(""); + callback?.(); + }; + return ( + + +
+

+ {message} +

+
+ + + + + +
+
+ ); +} + +export { AlertDialog }; diff --git a/apps/fo/src/shared/ui/confirm-dialog.tsx b/apps/fo/src/shared/ui/confirm-dialog.tsx new file mode 100644 index 000000000..6a4996bfb --- /dev/null +++ b/apps/fo/src/shared/ui/confirm-dialog.tsx @@ -0,0 +1,48 @@ +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Button, + type ButtonProps, +} from "@common/components"; + +type ConfirmDialogProps = { + isOpen?: boolean; + confirmButtonProps?: ButtonProps & { label?: string }; + cancelButtonProps?: ButtonProps & { label?: string }; +}; + +function ConfirmDialog({ + isOpen, + confirmButtonProps, + cancelButtonProps, +}: ConfirmDialogProps) { + const { message, isOpen: isGlobalOpen, onConfirm, onCancel } = useConfirm(); + return ( + + +
+

+ {message} +

+
+ + + + + + + + +
+
+ ); +} + +export { ConfirmDialog }; diff --git a/apps/fo/src/shared/ui/map-layout.tsx b/apps/fo/src/shared/ui/map-layout.tsx new file mode 100644 index 000000000..f743defbe --- /dev/null +++ b/apps/fo/src/shared/ui/map-layout.tsx @@ -0,0 +1,13 @@ +//import { useMenuStore } from "../lib/use-menu-store"; + +function MapLayout({ children }: { children: React.ReactNode }) { + //const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); + + return ( +
+ {children} +
+ ); +} + +export { MapLayout }; diff --git a/apps/fo/src/shared/ui/page-layout.tsx b/apps/fo/src/shared/ui/page-layout.tsx new file mode 100644 index 000000000..358d3b5db --- /dev/null +++ b/apps/fo/src/shared/ui/page-layout.tsx @@ -0,0 +1,22 @@ +import { PageTitle } from "@common/components"; +import { useMenuStore } from "../lib/use-menu-store"; + +function PageLayout({ children }: { children: React.ReactNode }) { + const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); + + const menuPaths = currentMenuInfo?.menuPath + ?.split(">") + .map((path) => path.trim()); + + return ( +
+ + {children} +
+ ); +} + +export { PageLayout }; diff --git a/apps/fo/src/shared/ui/prompt-dialog.tsx b/apps/fo/src/shared/ui/prompt-dialog.tsx new file mode 100644 index 000000000..496a18b5b --- /dev/null +++ b/apps/fo/src/shared/ui/prompt-dialog.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Input, +} from "@common/components"; +import { usePrompt } from "@/src/shared/lib/use-prompt"; + +function PromptDialog() { + const { message, isOpen, onSubmit, onCancel } = usePrompt(); + const [value, setValue] = useState(""); + + return ( + + +
+

+ {message} +

+ setValue(e.target.value)} + /> +
+ + + + + + + + +
+
+ ); +} + +export { PromptDialog }; diff --git a/apps/fo/tailwind.config.ts b/apps/fo/tailwind.config.ts index 33ac9204f..91db77945 100644 --- a/apps/fo/tailwind.config.ts +++ b/apps/fo/tailwind.config.ts @@ -9,4 +9,11 @@ export default { "../../packages/common/business_components/src/ui/**/*.{js,ts,jsx,tsx,mdx}", "../../packages/common/components/src/ui/**/*.{js,ts,jsx,tsx,mdx}", ], + theme: { + extend: { + boxShadow: { + blur: "0 0 10px -1px rgba(0, 0, 0, 0.06)", + }, + }, + }, } satisfies Config; \ No newline at end of file diff --git a/apps/fo/tsconfig.json b/apps/fo/tsconfig.json index 493584782..59bd02446 100644 --- a/apps/fo/tsconfig.json +++ b/apps/fo/tsconfig.json @@ -28,12 +28,8 @@ "@/*": ["./*"], "@common/components/*": ["../../packages/common/components/src/*"], "@common/components": ["../../packages/common/components/src/index"], - "@common/business_components/*": [ - "../../packages/common/business_components/src/*" - ], - "@common/business_components": [ - "../../packages/common/business_components/src/index" - ], + "@common/business_components/*": ["../../packages/common/business_components/src/*"], + "@common/business_components": ["../../packages/common/business_components/src/index"], "@common/network": ["../../packages/common/network"] } }, diff --git a/apps/sn/.env.development b/apps/sn/.env.development index 24edce282..8e9cb096e 100644 --- a/apps/sn/.env.development +++ b/apps/sn/.env.development @@ -1,13 +1,13 @@ NEXT_PUBLIC_SITE_ID = SN +NEXT_PUBLIC_PORT = 3000 NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token -NEXT_PUBLIC_PORT = 11302 -NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11302 -NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 -NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver -NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver -NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g -NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com -NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083 \ No newline at end of file +NEXT_PUBLIC_PROXY_HOST = https://devlion.hasuon.or.kr +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl +NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC +NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/dc-service \ No newline at end of file diff --git a/apps/sn/.env.mindone b/apps/sn/.env.mindone new file mode 100644 index 000000000..f5212f6bb --- /dev/null +++ b/apps/sn/.env.mindone @@ -0,0 +1,13 @@ +NEXT_PUBLIC_SITE_ID = SN +NEXT_PUBLIC_PORT = 11302 +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token +NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11302 +NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g +NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083 +NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com \ No newline at end of file diff --git a/apps/sn/.env.production b/apps/sn/.env.production index 158f22aaf..d8c2e00e9 100644 --- a/apps/sn/.env.production +++ b/apps/sn/.env.production @@ -1,13 +1,13 @@ NEXT_PUBLIC_SITE_ID = SN +NEXT_PUBLIC_PORT = 3000 NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token -NEXT_PUBLIC_PORT = 3000 -NEXT_PUBLIC_PROXY_HOST = https://sn.hasuon.or.kr +NEXT_PUBLIC_PROXY_HOST = https://lion.hasuon.or.kr +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC -NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 -NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/dc-service \ No newline at end of file diff --git a/apps/sn/src/entities/auth/lib/auth-wrapper.tsx b/apps/sn/src/entities/auth/lib/auth-wrapper.tsx index cfe908e2a..5d93b000d 100644 --- a/apps/sn/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/sn/src/entities/auth/lib/auth-wrapper.tsx @@ -9,8 +9,8 @@ import { ENDPOINT, AUTH_USER_ID } from "@/shared/config"; import { getCookie } from "@/shared/lib"; import { LoginApi } from "../api/login-service"; -const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; -const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_DEV_CM_LOGIN_PAGE_URL; // 개발 환경 URL + +const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_DEV_CM_LOGIN_PAGE_URL || "http://localhost:3000/auth/login"; // 로그인 페이지 URL const AuthWrapper = ({ children }: { children: React.ReactNode }) => { const router = useRouter(); diff --git a/apps/wr/.env.development b/apps/wr/.env.development index 769d229e2..34ea78105 100644 --- a/apps/wr/.env.development +++ b/apps/wr/.env.development @@ -1,31 +1,20 @@ -# NEXT_PUBLIC_SITE_ID = WR -# NEXT_PUBLIC_PORT = 11301 -# NEXT_PUBLIC_PROXY_HOST = http://localhost:3001 -# NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 -# NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 -# NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 -# NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload -# NEXT_PUBLIC_CLAIM_NAME = Authorization -# NEXT_PUBLIC_AUTH_USER_ID = token-id -# NEXT_PUBLIC_ACCESS_TOKEN = access-token -# NEXT_PUBLIC_REFRESH_TOKEN = refresh-token - +# 공단 개발 NEXT_PUBLIC_SITE_ID = WR -NEXT_PUBLIC_PORT = 11301 -NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11301 - NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 -NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 -NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 -NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 -NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload -NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver -# NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver -NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +NEXT_PUBLIC_PORT = 3000 NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token -NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g -NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API +NEXT_PUBLIC_PROXY_HOST = https://devtiger.hasuon.or.kr +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl +NEXT_PUBLIC_WR_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/reuse-service/api/v1 +NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/sewerage-service/api +NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC + NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do -NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM= +NEXT_PRIVATE_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA= \ No newline at end of file diff --git a/apps/wr/.env.local b/apps/wr/.env.local index df38635ea..e3c0e1daf 100644 --- a/apps/wr/.env.local +++ b/apps/wr/.env.local @@ -1,29 +1,21 @@ NEXT_PUBLIC_SITE_ID = WR NEXT_PUBLIC_PORT = 3001 -NEXT_PUBLIC_PROXY_HOST = http://localhost:3001 -NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 -NEXT_PUBLIC_SERVER_API_URL=http://dev.onasys.co.kr:8000 -NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 -NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 -NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token +NEXT_PUBLIC_PROXY_HOST = http://localhost:3001 +NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver -# NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g - - - -NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do +NEXT_PUBLIC_FINDADRESS_URL = https://business.juso.go.kr/addrlink/addrLinkApi.do NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM= - - -NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API -#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성 -#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정 - +REACT_EDITOR = atom diff --git a/apps/wr/.env.mindone b/apps/wr/.env.mindone new file mode 100644 index 000000000..f7d7445ce --- /dev/null +++ b/apps/wr/.env.mindone @@ -0,0 +1,19 @@ +NEXT_PUBLIC_SITE_ID = WR +NEXT_PUBLIC_PORT = 11301 +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token +NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11301 +NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API +NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g + +NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do +NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM= diff --git a/apps/wr/.env.production b/apps/wr/.env.production index e5900440f..7086521c6 100644 --- a/apps/wr/.env.production +++ b/apps/wr/.env.production @@ -1,29 +1,20 @@ +# 공단 운영 NEXT_PUBLIC_SITE_ID = WR NEXT_PUBLIC_PORT = 3000 -NEXT_PUBLIC_PROXY_HOST = http://wr.hasuon.or.kr -NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000 -NEXT_PUBLIC_FO_SERVER_API_URL = https://wr.hasuon.or.kr/operation-service/api/v1 -NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000 -NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload -NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver -NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token -NEXT_PUBLIC_WR_SERVER_API_URL = https://wr.hasuon.or.kr/reuse-service/api/v1 -NEXT_PUBLIC_SEWERAGE_API_URL = https://wr.hasuon.or.kr/sewerage-service/api +NEXT_PUBLIC_PROXY_HOST = https://tiger.hasuon.or.kr +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl +NEXT_PUBLIC_WR_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/reuse-service/api/v1 +NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/sewerage-service/api NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC -#NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/api/v1 -#NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/operation-service/api/v1 -#NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver -#NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api - NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do -NEXT_PRIVATE_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA= - - - - - +NEXT_PRIVATE_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA= \ No newline at end of file diff --git a/apps/wr/app/(with-layout)/drgtSttn/page.tsx b/apps/wr/app/(with-layout)/matMng/coMng/drgtSttn/page.tsx similarity index 100% rename from apps/wr/app/(with-layout)/drgtSttn/page.tsx rename to apps/wr/app/(with-layout)/matMng/coMng/drgtSttn/page.tsx diff --git a/apps/wr/app/layout.tsx b/apps/wr/app/layout.tsx index f728f5de0..25530bfef 100644 --- a/apps/wr/app/layout.tsx +++ b/apps/wr/app/layout.tsx @@ -1,49 +1,14 @@ -"use client"; -import "./globals.css"; -import i18next from "i18next"; -import { I18nextProvider } from "react-i18next"; -import koBase from "../public/locales/ko/message.json"; -import { StoreProvider } from "./store"; -import { LoginApi } from "@/src/entities/auth"; -import { useEffect } from "react"; +import type { Metadata } from "next"; +import { App } from "../src/app"; -const i18n = i18next.createInstance(); -i18n.init({ - lng: "ko", - fallbackLng: "ko", - resources: { - ko: { - base: koBase, - }, - }, -}); +export const metadata: Metadata = { + title: "디지털 하수관로", + description: "디지털 하수관로 시스템입니다", + keywords: ["하수관로"], + openGraph: { + title: "디지털 하수관로", + description: "디지털 하수관로 시스템입니다" + } +}; -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - - useEffect(() => { - // 렌더 중이 아니라 클라이언트에서만 실행 + 에러 캐치 - void LoginApi.login({ userId: "CLEANWATER", password: "gktneh12!@" }) - .finally(()=>{ - // getCommonUpCd() - } ) - .catch((e) => { - console.error("Auto login failed:", e); - }); - }, []); - - return ( - - - - - {children} - - - - - ); -} +export default App; \ No newline at end of file diff --git a/apps/wr/app/page.tsx b/apps/wr/app/page.tsx index 8729ac5f0..37fbe8b00 100644 --- a/apps/wr/app/page.tsx +++ b/apps/wr/app/page.tsx @@ -1,166 +1,31 @@ "use client"; -import { Button } from "@common/components/ui"; + +import { LoginApi } from "../src/entities/auth"; +import { useEffect } from "react"; +import { getCookie } from "../src/shared/lib"; +import { AUTH_USER_ID } from "../src/shared/config"; import { useRouter } from "next/navigation"; + export default function Home() { const router = useRouter(); - return ( - <> -
- {/* 시설 등록 */} -
-

시설 등록

-
-
- -
-
- -
-
- -
-
- -
-
-
+ const userId = "CLEANWATER"; + const password = "gktneh12!@"; + const checkToken = getCookie(AUTH_USER_ID); - {/* 지도점검 결과 */} -
-

지도점검 결과

-
-
- -
-
- -
-
- -
-
- -
-
-
- - {/* 시설 현황 */} -
-

시설 현황

-
-
- -
-
- -
-
- -
-
- -
-
-
- - {/* 수요처 관리 */} -
-

수요처 관리

-
- -
- -
-
- -
-
- -
-
-
- - {/* 수요처 매칭 */} -
-

수요처 매칭

-
-
- -
-
- -
-
- -
-
-
- {/* 통계*/} -
-

통계 매칭

-
-
- -
-
- -
-
-
- - {/* 시설 현황(지도) */} -
-

시설 현황(지도)

-
-
- -
-
-
-
- - ); + useEffect(() => { + if (!checkToken) { + async function doLogin() { + try { + await LoginApi.login({ userId, password }); + window.location.href = "/map/mainMap"; + } catch (err) { + console.error("로그인 실패", err); + } + } + doLogin(); + } else { + router.push("/map/mainMap"); + } + }, []); } diff --git a/apps/wr/app/store/common/auth/index.tsx b/apps/wr/app/store/common/auth/index.tsx index 5e65f3b6a..7eceacb39 100644 --- a/apps/wr/app/store/common/auth/index.tsx +++ b/apps/wr/app/store/common/auth/index.tsx @@ -39,8 +39,7 @@ export const authActionsExport: (set: any, get: any) => authActions = ( getMenuAuthrts: async (menuCd: number) => { try { const response = await getFetchWithCount( - fetchType.LOCAL, - + fetchType.CS, `/cms-service/api/v1/menu-authrts/pageinfo/${menuCd}` ); set((state: authState) => { diff --git a/apps/wr/app/store/common/code/index.ts b/apps/wr/app/store/common/code/index.ts index 0601c5e41..f6e28339c 100644 --- a/apps/wr/app/store/common/code/index.ts +++ b/apps/wr/app/store/common/code/index.ts @@ -42,7 +42,7 @@ export const getCommonUpCd = async (set: any) => { try { let lwCdCode: Record = {}; const upCd = await getFetchWithCount( - fetchType.LOCAL, + fetchType.CS, "/cms-service/api/v1/up-codes?keyword=&upCd=&cdSe=&useYn=Y&siteId=WR&page=0&size=100&sort=string" ); @@ -56,7 +56,7 @@ export const getCommonUpCd = async (set: any) => { if (!mapping) return; const lwCd = await getFetchWithCount( - fetchType.LOCAL, + fetchType.CS, `/cms-service/api/v1/lwpo-cds/up-cd/${cd}` ); diff --git a/apps/wr/components/lib/api/index.ts b/apps/wr/components/lib/api/index.ts index 6b1f4292c..dc1147abb 100644 --- a/apps/wr/components/lib/api/index.ts +++ b/apps/wr/components/lib/api/index.ts @@ -4,6 +4,7 @@ import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config"; import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "../config/"; import { getCookie } from "@common/components/lib/cookie"; +const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST; export const fetchType = { COMMON: "common", WR: "wr", @@ -20,15 +21,15 @@ export const getFetch = async (type: string, url: string) => { : type === fetchType.CS ? "" : DEV_URL_WR; - + const apiUrl = PROXY_HOST + div + url; const tokenId = getCookie(AUTH_USER_ID); const Authorization = getCookie(CLAIM_NAME); - const response = await fetch(div + url, { + const response = await fetch(apiUrl, { method: "GET", headers: { accept: "application/json", - AUTH_USER_ID: tokenId ?? "", - CLAIM_NAME: Authorization ?? "", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, }); @@ -71,15 +72,16 @@ export const getFetchWithCount = async (type: string, url: string) => { : type === fetchType.CS ? "" : DEV_URL_WR; + const apiUrl = PROXY_HOST + div + url; const tokenId = getCookie(AUTH_USER_ID); const Authorization = getCookie(CLAIM_NAME); - const response = await fetch(div + url, { + const response = await fetch(apiUrl, { method: "GET", headers: { accept: "application/json", - AUTH_USER_ID: tokenId ?? "", - CLAIM_NAME: Authorization ?? "", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, }); @@ -116,16 +118,16 @@ export const getFetchWithCount = async (type: string, url: string) => { export const postFectch = async (url: string, params: any, type?: string) => { const div = type === fetchType.COMMON ? DEV_URL_FO : DEV_URL_WR; - const tokenId = getCookie(AUTH_USER_ID); const Authorization = getCookie(CLAIM_NAME); - const response = await fetch(div + url, { + const apiUrl = PROXY_HOST + div + url; + const response = await fetch(apiUrl, { method: "POST", headers: { accept: "application/json", "Content-Type": "application/json", - AUTH_USER_ID: tokenId ?? "", - CLAIM_NAME: Authorization ?? "", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); @@ -144,16 +146,16 @@ export const putFectch = async (url: string, params: any, type?: string) => { : type === fetchType.CS ? "" : DEV_URL_WR; - + const apiUrl = PROXY_HOST + div + url; const tokenId = getCookie(AUTH_USER_ID); const Authorization = getCookie(CLAIM_NAME); - const response = await fetch(div + url, { + const response = await fetch(apiUrl, { method: "PUT", headers: { "Content-Type": "application/json", - AUTH_USER_ID: tokenId ?? "", - CLAIM_NAME: Authorization ?? "", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); @@ -176,15 +178,16 @@ export const deleteFectch = async ( : type === fetchType.CS ? "" : DEV_URL_WR; + const apiUrl = PROXY_HOST + div + url; const tokenId = getCookie(AUTH_USER_ID); const Authorization = getCookie(CLAIM_NAME); - const response = await fetch(div + url, { + const response = await fetch(apiUrl, { method: "DELETE", headers: { "Content-Type": "application/json", - AUTH_USER_ID: tokenId ?? "", - CLAIM_NAME: Authorization ?? "", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: params ? JSON.stringify(params) : undefined, }); diff --git a/apps/wr/components/lib/config/index.ts b/apps/wr/components/lib/config/index.ts index f05a38e62..979862f12 100644 --- a/apps/wr/components/lib/config/index.ts +++ b/apps/wr/components/lib/config/index.ts @@ -2,9 +2,9 @@ import { PROXY_HOST, SERVER_API_CS_TUS_URL, SERVER_API_CS_URL, - SERVER_API_FO_URL, - SERVER_API_WR_URL, - SERVER_SEWERAGE_API_URL, + //SERVER_API_FO_URL, + //SERVER_API_WR_URL, + //SERVER_SEWERAGE_API_URL, } from "@/src/shared/config"; //const DEV_PORT = "11080"; @@ -18,9 +18,10 @@ import { //export const CS_TUS_UPLOAD= "/attach-service/api/v1/tus/upload"; export const LOCAL_URL_WR = PROXY_HOST + `/api/v1`; -export const DEV_URL_WR = SERVER_API_WR_URL; -export const DEV_URL_FO = SERVER_API_FO_URL; -export const DEV_URL_SN = SERVER_SEWERAGE_API_URL; +export const DEV_URL_WR = '/reuse-service'; +export const DEV_URL_FO = '/operation-service'; +//export const DEV_URL_FO = SERVER_API_FO_URL; +export const DEV_URL_SN = '/sewerage-service/api'; export const LOCAL_URL = PROXY_HOST; export const CS_URL = SERVER_API_CS_URL; export const CS_TUS_UPLOAD = SERVER_API_CS_TUS_URL; diff --git a/apps/wr/middleware.ts b/apps/wr/middleware.ts index 5dc77eab4..1b6370cc0 100644 --- a/apps/wr/middleware.ts +++ b/apps/wr/middleware.ts @@ -18,6 +18,9 @@ export function middleware(req: NextRequest) { console.log(`[Middleware] Request URL: ${req.url}`); const searchParams = req.nextUrl.searchParams; + // if (pathname === "/") + // return NextResponse.redirect(new URL("/map/mainMap", req.url)); + if (pathname.startsWith("/server")) { const decodedParams = Array.from(searchParams.entries()) .map(([key, value]) => `${key}=${decodeURIComponent(value)}`) @@ -29,10 +32,6 @@ export function middleware(req: NextRequest) { return NextResponse.rewrite(new URL(newUrl)); } - if (pathname === "/") { - return NextResponse.redirect(new URL("/map/mainMap", req.url)); - } - if (pathname.startsWith("/cms-service")) { const requestHeaders = new Headers(req.headers); const Authorization = req.cookies.get(CLAIM_NAME)?.value; diff --git a/apps/wr/src/app/index.tsx b/apps/wr/src/app/index.tsx new file mode 100644 index 000000000..692b55ef0 --- /dev/null +++ b/apps/wr/src/app/index.tsx @@ -0,0 +1,20 @@ +import "./styles/base.css"; +import "./styles/components.css"; +import "@fontsource-variable/noto-sans-kr"; + +import { AppProvider } from "./providers"; +import { cookies } from "next/headers"; + +const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => { + const theme = (await cookies()).get("site-theme-key"); + + return ( + + + {children} + + + ); +}; + +export { App }; diff --git a/apps/wr/src/app/providers/app-provider.tsx b/apps/wr/src/app/providers/app-provider.tsx new file mode 100644 index 000000000..0497e1f52 --- /dev/null +++ b/apps/wr/src/app/providers/app-provider.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { DndProvider } from "react-dnd"; +import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { ConfirmProvider } from "@frontend-opensource/use-react-hooks"; +import { AuthWrapper } from "../../entities/auth"; +import { MenuWrapper } from "../../entities/menu"; +import { PromptProvider, StoreProvider } from "../../shared/providers"; +import { AlertDialog } from "../../shared/ui/alert-dialog"; +import { PromptDialog } from "../../shared/ui/prompt-dialog"; +import { ConfirmDialog } from "../../shared/ui/confirm-dialog"; +// import "./globals.css"; +import i18next from "i18next"; +import { I18nextProvider } from "react-i18next"; +import koBase from "@/public/locales/ko/message.json"; +import { StoreProvider as WrStoreProvider } from "@/app/store"; +//import { LoginApi } from "@/src/entities/auth"; +//import { useEffect } from "react"; + + +const i18n = i18next.createInstance(); +i18n.init({ + lng: "ko", + fallbackLng: "ko", + resources: { + ko: { + base: koBase, + }, + }, +}); + +const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { + const queryClient = new QueryClient(); + return ( + + + + + + + + + + {children} + + + + + + + + + + + +
+ +
+
+ ); +}; + +export { AppProvider }; diff --git a/apps/wr/src/app/providers/index.ts b/apps/wr/src/app/providers/index.ts new file mode 100644 index 000000000..15f99b7fd --- /dev/null +++ b/apps/wr/src/app/providers/index.ts @@ -0,0 +1 @@ +export { AppProvider } from "./app-provider"; diff --git a/apps/wr/src/app/styles/base.css b/apps/wr/src/app/styles/base.css new file mode 100644 index 000000000..8453ff9e8 --- /dev/null +++ b/apps/wr/src/app/styles/base.css @@ -0,0 +1,12 @@ +@import "@common/assets/styles/base.css"; +@import "@common/assets/styles/diveicon.css"; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body { + font-family: "Noto Sans KR", sans-serif; + font-size: 10px; +} diff --git a/apps/wr/src/app/styles/components.css b/apps/wr/src/app/styles/components.css new file mode 100644 index 000000000..4dc30bafd --- /dev/null +++ b/apps/wr/src/app/styles/components.css @@ -0,0 +1,29 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="black"] { + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/wr/src/entities/auth/index.ts b/apps/wr/src/entities/auth/index.ts index 9dc9dd368..97e142225 100644 --- a/apps/wr/src/entities/auth/index.ts +++ b/apps/wr/src/entities/auth/index.ts @@ -1,2 +1,2 @@ export { LoginApi } from "./api/login-service"; -//export { AuthWrapper } from "./lib/auth-wrapper"; +export { AuthWrapper } from "./lib/auth-wrapper"; diff --git a/apps/wr/src/entities/auth/lib/auth-wrapper.tsx b/apps/wr/src/entities/auth/lib/auth-wrapper.tsx new file mode 100644 index 000000000..94d39b6e4 --- /dev/null +++ b/apps/wr/src/entities/auth/lib/auth-wrapper.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { client } from "../../../shared/api"; +import type { IUserInfo } from "../../../shared/store"; +import { useUserStore } from "../../../shared/lib"; +import { ENDPOINT, AUTH_USER_ID } from "../../../shared/config"; +import { getCookie } from "../../../shared/lib"; +import { LoginApi } from "../api/login-service"; + + +//const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_DEV_CM_LOGIN_PAGE_URL || "http://localhost:3000/auth/login"; // 로그인 페이지 URL + +const AuthWrapper = ({ children }: { children: React.ReactNode }) => { + const router = useRouter(); + const { userInfo, setUserInfo } = useUserStore((state) => state); + + async function authCheck() { + const userId = getCookie(AUTH_USER_ID); + if (!userId) { + throw new Error("Missing required authentication tokens"); + } + + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/info/me`, + ); + if (!userInfo) setUserInfo(response.data); + } + + useEffect(() => { + (async () => { + try { + await authCheck(); + } catch (error) { + console.error(error); + + try { + await LoginApi.silentRefresh(); + await authCheck(); + } catch (refreshError) { + console.error("Token refresh failed:", refreshError); + setUserInfo(null); + router.push("/"); // 개발시 홈으로 돌아가기 + + // window.location.href = LOGIN_PAGE_URL; // 세션 만료 시 CM 로그인 페이지로 리다이렉트 + // router.push는 같은 Next.js 앱 내에서만 작동하므로 다른 앱으로 이동할 때는 window.location.href 사용 + } + } + })(); + }, []); + + return <>{children}; +}; + +export { AuthWrapper }; diff --git a/apps/wr/src/entities/menu-authority/@x/menu.ts b/apps/wr/src/entities/menu-authority/@x/menu.ts new file mode 100644 index 000000000..86479f859 --- /dev/null +++ b/apps/wr/src/entities/menu-authority/@x/menu.ts @@ -0,0 +1 @@ +export { MenuAuthorityApi } from "../api/menu-authority-service"; diff --git a/apps/wr/src/entities/menu-authority/@x/posting.ts b/apps/wr/src/entities/menu-authority/@x/posting.ts new file mode 100644 index 000000000..bfe84fa0d --- /dev/null +++ b/apps/wr/src/entities/menu-authority/@x/posting.ts @@ -0,0 +1 @@ +export { AuthorityButton } from "../ui/authority-button"; diff --git a/apps/wr/src/entities/menu-authority/api/menu-authority-service.ts b/apps/wr/src/entities/menu-authority/api/menu-authority-service.ts new file mode 100644 index 000000000..c7ac056f4 --- /dev/null +++ b/apps/wr/src/entities/menu-authority/api/menu-authority-service.ts @@ -0,0 +1,40 @@ +import { client } from "@/src/shared/api"; +import { ENDPOINT } from "@/src/shared/config"; +import type { + MenuAuthorityList, + MenuAuthorityParams, + MenuAuthorityPayload, + MenuAuthorityService, + PageMenuAuthority, +} from "../model/menu-authority-interface"; + +export class MenuAuthorityApiService implements MenuAuthorityService { + public async getMenuAuthorityList( + params: MenuAuthorityParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`, + { params }, + ); + return response.data; + } + + public async getPageMenuAuthority( + menuCd: number, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}/pageinfo/${menuCd}`, + ); + return response.data; + } + + public async saveMenuAuthority(data: MenuAuthorityPayload[]): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`, + data, + ); + return response.data; + } +} + +export const MenuAuthorityApi = new MenuAuthorityApiService(); diff --git a/apps/wr/src/entities/menu-authority/index.ts b/apps/wr/src/entities/menu-authority/index.ts new file mode 100644 index 000000000..2a555b77e --- /dev/null +++ b/apps/wr/src/entities/menu-authority/index.ts @@ -0,0 +1,2 @@ +export { AuthorityButton } from "./ui/authority-button"; +export { ManagementAuthorityDialog } from "./ui/management-authority-dialog"; diff --git a/apps/wr/src/entities/menu-authority/model/menu-authority-interface.ts b/apps/wr/src/entities/menu-authority/model/menu-authority-interface.ts new file mode 100644 index 000000000..b59574c2f --- /dev/null +++ b/apps/wr/src/entities/menu-authority/model/menu-authority-interface.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface MenuAuthorityParams extends PageableParams { + groupId?: string; + menuCd?: number; + useYn?: string; +} + +export interface PageMenuAuthority { + mngAuthrtYn: string; + inqAuthrtYn: string; + regAuthrtYn: string; + mdfcnAuthrtYn: string; + delAuthrtYn: string; +} + +export const menuAuthoritySchema = z.object({ + groupId: z.string(), + groupNm: z.string(), + menuCd: z.number(), + mngAuthrtYn: z.string(), + inqAuthrtYn: z.string(), + regAuthrtYn: z.string(), + mdfcnAuthrtYn: z.string(), + delAuthrtYn: z.string(), + useYn: z.string(), + mode: z.string(), +}); + +export const menuAuthorityPayloadSchema = menuAuthoritySchema.omit({ + groupNm: true, +}); + +export const menuAuthorityFormSchema = z.object({ + menuAuthority: z.array(menuAuthorityPayloadSchema), +}); + +export type MenuAuthority = z.infer; +export type MenuAuthorityList = PageableList; +export type MenuAuthorityPayload = z.infer; +export type MenuAuthorityFormData = z.infer; + +export interface MenuAuthorityService { + getMenuAuthorityList(params: MenuAuthorityParams): Promise; + getPageMenuAuthority(menuCd: number): Promise; + saveMenuAuthority(data: MenuAuthorityPayload[]): Promise; +} diff --git a/apps/wr/src/entities/menu-authority/model/menu-authority-table-columns.tsx b/apps/wr/src/entities/menu-authority/model/menu-authority-table-columns.tsx new file mode 100644 index 000000000..6969a85e9 --- /dev/null +++ b/apps/wr/src/entities/menu-authority/model/menu-authority-table-columns.tsx @@ -0,0 +1,117 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import type { UseFormReturn } from "react-hook-form"; +import { Checkbox } from "@common/components"; +import type { + MenuAuthority, + MenuAuthorityFormData, + MenuAuthorityPayload, +} from "./menu-authority-interface"; + +export const createColumns = ( + form: UseFormReturn, +): ColumnDef[] => { + const getCurrentRow = (i: number) => form.watch("menuAuthority")[i]; + + const handleCheckedChange = + (i: number, key: keyof MenuAuthorityPayload) => (checked: boolean) => { + if (getCurrentRow(i)?.mode === "R") { + form.setValue(`menuAuthority.${i}.mode`, "U"); + } + form.setValue(`menuAuthority.${i}.${key}`, checked ? "Y" : "N"); + }; + + return [ + { + accessorKey: "groupNm", + header: "그룹명", + }, + { + accessorKey: "mngAuthrtYn", + header: "관리권한", + cell: ({ row }) => { + return ( + { + if (checked) { + handleCheckedChange( + row.index, + "regAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "mdfcnAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "delAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "inqAuthrtYn", + )(checked as boolean); + } + handleCheckedChange(row.index, "mngAuthrtYn")(checked as boolean); + }} + /> + ); + }, + }, + { + accessorKey: "regAuthrtYn", + header: "등록권한", + cell: ({ row }) => { + return ( + { + if (checked) { + handleCheckedChange( + row.index, + "inqAuthrtYn", + )(checked as boolean); + } + handleCheckedChange(row.index, "regAuthrtYn")(checked as boolean); + }} + /> + ); + }, + }, + { + accessorKey: "mdfcnAuthrtYn", + header: "수정권한 ", + cell: ({ row }) => { + return ( + + ); + }, + }, + { + accessorKey: "delAuthrtYn", + header: "삭제권한", + cell: ({ row }) => { + return ( + + ); + }, + }, + { + accessorKey: "inqAuthrtYn", + header: "조회권한", + cell: ({ row }) => { + return ( + + ); + }, + }, + ]; +}; diff --git a/apps/wr/src/entities/menu-authority/ui/authority-button.tsx b/apps/wr/src/entities/menu-authority/ui/authority-button.tsx new file mode 100644 index 000000000..257ab6e5d --- /dev/null +++ b/apps/wr/src/entities/menu-authority/ui/authority-button.tsx @@ -0,0 +1,26 @@ +import { useMenuStore } from "@/shared/lib"; +import { Button, cn, type ButtonProps } from "@common/components"; +import type { PageMenuAuthority } from "../model/menu-authority-interface"; + +type AuthorityButtonProps = { + authority: keyof PageMenuAuthority; +} & ButtonProps; + +function AuthorityButton({ + authority, + className, + children, + ...props +}: AuthorityButtonProps) { + const pageMenuAuthority = useMenuStore((state) => state.pageMenuAuthority); + + const isAuthority = pageMenuAuthority?.[authority] === "Y"; + + return ( + + ); +} + +export { AuthorityButton }; diff --git a/apps/wr/src/entities/menu-authority/ui/management-authority-dialog.tsx b/apps/wr/src/entities/menu-authority/ui/management-authority-dialog.tsx new file mode 100644 index 000000000..4a73cb596 --- /dev/null +++ b/apps/wr/src/entities/menu-authority/ui/management-authority-dialog.tsx @@ -0,0 +1,103 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; +import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + DataTable, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { + menuAuthorityFormSchema, + type MenuAuthorityFormData, + type MenuAuthorityPayload, +} from "../model/menu-authority-interface"; +import { createColumns } from "../model/menu-authority-table-columns"; +import { MenuAuthorityApi } from "../api/menu-authority-service"; + +type ManagementAuthorityDialogProps = { + menuCd?: number; +} & React.ComponentProps; + +function ManagementAuthorityDialog({ + menuCd, + ...props +}: ManagementAuthorityDialogProps) { + const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); + + const { data: menuAuthorityList } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, menuCd], + queryFn: () => MenuAuthorityApi.getMenuAuthorityList({ menuCd }), + enabled: !!menuCd, + }); + + const { mutateAsync: saveMenuAuthority } = useMutationWrapper({ + mutationFn: (data: MenuAuthorityPayload[]) => + MenuAuthorityApi.saveMenuAuthority(data), + onSuccess: () => { + alert("저장되었습니다."); + }, + }); + + const form = useForm({ + resolver: zodResolver(menuAuthorityFormSchema), + values: { + menuAuthority: menuAuthorityList?.content ?? [], + }, + }); + + const table = useReactTable({ + data: menuAuthorityList?.content ?? [], + columns: createColumns(form), + getCoreRowModel: getCoreRowModel(), + }); + + const handleSave = async () => { + if (await confirm("저장하시겠습니까?")) { + const data = form.getValues("menuAuthority"); + const filteredData = data.filter((item) => item.mode !== "R"); + await saveMenuAuthority(filteredData); + } + }; + + return ( + + + + + + + 그룹별 메뉴 권한 + +
+ +
+ + + + + + +
+
+ ); +} + +export { ManagementAuthorityDialog }; diff --git a/apps/wr/src/entities/menu/api/menu-service.ts b/apps/wr/src/entities/menu/api/menu-service.ts new file mode 100644 index 000000000..8b08d7fa5 --- /dev/null +++ b/apps/wr/src/entities/menu/api/menu-service.ts @@ -0,0 +1,14 @@ +import { client } from "@/src/shared/api"; +import { ENDPOINT } from "@/src/shared/config"; +import type { MenuItem, MenuService } from "../model/menu-interface"; + +export class MenuApiService implements MenuService { + async getMenuRoles(siteId: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`, + ); + return response.data; + } +} + +export const MenuApi = new MenuApiService(); diff --git a/apps/wr/src/entities/menu/index.ts b/apps/wr/src/entities/menu/index.ts new file mode 100644 index 000000000..04f63e38b --- /dev/null +++ b/apps/wr/src/entities/menu/index.ts @@ -0,0 +1,2 @@ +export { MenuWrapper } from "./lib/menu-wrapper"; +export type { MenuItem } from "./model/menu-interface"; diff --git a/apps/wr/src/entities/menu/lib/menu-wrapper.tsx b/apps/wr/src/entities/menu/lib/menu-wrapper.tsx new file mode 100644 index 000000000..f445123d3 --- /dev/null +++ b/apps/wr/src/entities/menu/lib/menu-wrapper.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useEffect } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { MenuAuthorityApi } from "@/src/entities/menu-authority/@x/menu"; +import { useAlertStore, useMenuStore } from "@/src/shared/lib"; +import { getCookie } from "@/src/shared/lib";; +import { SITE_ID } from "@/src/shared/config"; +import { MenuApi } from "../api/menu-service"; +import { getCurrentMenu } from "./utils"; + +const MenuWrapper = ({ children }: { children: React.ReactNode }) => { + const router = useRouter(); + const pathname = usePathname(); + + const alert = useAlertStore((state) => state.setMessage); + const { pageMenuAuthority, setCurrentMenuInfo, setPageMenuAuthority } = + useMenuStore((state) => state); + + const getCurrentMenuInfo = async () => { + const menuRoles = await MenuApi.getMenuRoles(SITE_ID); + if (!menuRoles) return; + return getCurrentMenu(menuRoles, pathname ?? ""); + }; + + const getPageMenuAuthority = async () => { + const menuCd = getCookie("menu-code-key"); + if (!menuCd) return; + const response = await MenuAuthorityApi.getPageMenuAuthority( + parseInt(menuCd), + ); + return response; + }; + + const checkInquiryAuthority = () => { + return pageMenuAuthority?.inqAuthrtYn === "Y"; + }; + + useEffect(() => { + (async () => { + const currentMenu = await getCurrentMenuInfo(); + if (currentMenu) setCurrentMenuInfo(currentMenu); + + const response = await getPageMenuAuthority(); + if (response?.length) setPageMenuAuthority(response[0]!); + })(); + }, []); + + useEffect(() => { + if (!pageMenuAuthority) return; + const inquiryAuthority = checkInquiryAuthority(); + if (!inquiryAuthority) + alert("페이지에 대한 조회 권한이 없습니다.", () => router.push("/")); + }, [pageMenuAuthority]); + + return <>{children}; +}; + +export { MenuWrapper }; diff --git a/apps/wr/src/entities/menu/lib/utils.ts b/apps/wr/src/entities/menu/lib/utils.ts new file mode 100644 index 000000000..1cf4514d9 --- /dev/null +++ b/apps/wr/src/entities/menu/lib/utils.ts @@ -0,0 +1,20 @@ +import type { MenuItem } from "../model/menu-interface"; + +export const getCurrentMenu = ( + menus: MenuItem[], + pathname: string, +): MenuItem | null => { + for (const menu of menus) { + if ( + menu.urlPath !== "" && + (menu.urlPath === pathname || pathname.startsWith(menu.urlPath)) + ) + return menu; + + if (menu.children) { + const children = getCurrentMenu(menu.children, pathname); + if (children) return children; + } + } + return null; +}; diff --git a/apps/wr/src/entities/menu/model/menu-interface.ts b/apps/wr/src/entities/menu/model/menu-interface.ts new file mode 100644 index 000000000..12c7d007a --- /dev/null +++ b/apps/wr/src/entities/menu/model/menu-interface.ts @@ -0,0 +1,23 @@ +export interface MenuItem { + menuCd: number; + menuKornNm: string; + menuEngNm: string; + parentId: number | null; + sortSeq: number; + iconNm?: string; + children: MenuItem[] | null; + levelNo: number; + urlPath: string; + menuLnkgTypeCd: string; + menuLnkgSn: number; + menuPath: string; + qrcd: string; + useYn: string; + otptYn: string; + newWndwYn: string; + requiredUrlPath: boolean; +} + +export interface MenuService { + getMenuRoles(siteId: string): Promise; +} diff --git a/apps/wr/src/shared/api/client.ts b/apps/wr/src/shared/api/client.ts new file mode 100644 index 000000000..cf37e6671 --- /dev/null +++ b/apps/wr/src/shared/api/client.ts @@ -0,0 +1,11 @@ +import { Client } from "@common/network"; +import { BASE_URL } from "../config/api"; +import { SITE_ID } from "../config/env"; +import { setCookie } from "../lib"; + +setCookie("site-id-key", SITE_ID); + +export const client = Client.getInstance({ + baseURL: BASE_URL, + siteId: SITE_ID, +}); diff --git a/apps/wr/src/shared/api/index.ts b/apps/wr/src/shared/api/index.ts new file mode 100644 index 000000000..b7b9d6121 --- /dev/null +++ b/apps/wr/src/shared/api/index.ts @@ -0,0 +1 @@ +export { client } from "./client"; diff --git a/apps/wr/src/shared/lib/index.ts b/apps/wr/src/shared/lib/index.ts new file mode 100644 index 000000000..b8b4c52c0 --- /dev/null +++ b/apps/wr/src/shared/lib/index.ts @@ -0,0 +1,6 @@ +export { useMutationWrapper } from "./use-mutation-wrapper"; +export { useAlertStore } from "./use-alert-store"; +export { useMenuStore } from "./use-menu-store"; +export { useUserStore } from "./use-user-store"; +export { usePrompt } from "./use-prompt"; +export { getCookie, setCookie, removeCookie } from "./cookie"; diff --git a/apps/wr/src/shared/lib/use-alert-store.ts b/apps/wr/src/shared/lib/use-alert-store.ts new file mode 100644 index 000000000..c21a00ed3 --- /dev/null +++ b/apps/wr/src/shared/lib/use-alert-store.ts @@ -0,0 +1,14 @@ +import { useStore } from "zustand"; +import { useContext } from "react"; +import type { AlertStore } from "../store/alert-store"; +import { StoreContext } from "../providers/store-provider"; + +export const useAlertStore = (selector: (store: AlertStore) => T): T => { + const storeContext = useContext(StoreContext); + + if (!storeContext) { + throw new Error("useAlertStore must be used within StoreProvider"); + } + + return useStore(storeContext.alertStore, selector); +}; diff --git a/apps/wr/src/shared/lib/use-menu-store.ts b/apps/wr/src/shared/lib/use-menu-store.ts new file mode 100644 index 000000000..66e41c76c --- /dev/null +++ b/apps/wr/src/shared/lib/use-menu-store.ts @@ -0,0 +1,14 @@ +import { useStore } from "zustand"; +import { useContext } from "react"; +import type { MenuStore } from "../store/menu-store"; +import { StoreContext } from "../providers/store-provider"; + +export const useMenuStore = (selector: (store: MenuStore) => T): T => { + const storeContext = useContext(StoreContext); + + if (!storeContext) { + throw new Error("useMenuStore must be used within StoreProvider"); + } + + return useStore(storeContext.menuStore, selector); +}; diff --git a/apps/wr/src/shared/lib/use-mutation-wrapper.tsx b/apps/wr/src/shared/lib/use-mutation-wrapper.tsx new file mode 100644 index 000000000..ce119d2a7 --- /dev/null +++ b/apps/wr/src/shared/lib/use-mutation-wrapper.tsx @@ -0,0 +1,31 @@ +import { AxiosError } from "axios"; +import { useMutation, type UseMutationOptions } from "@tanstack/react-query"; +import { useAlertStore } from "./use-alert-store"; + +export const useMutationWrapper = < + TData = unknown, + TError = unknown, + TVariables = void, + TContext = unknown, +>( + mutationOptions: UseMutationOptions, +) => { + const alert = useAlertStore((state) => state.setMessage); + return useMutation({ + ...mutationOptions, + onError: (error) => { + let errorMessage = ""; + + if (error instanceof AxiosError) { + if (error.response?.status) + errorMessage += `status: ${error.response.status}\n`; + if (error.response?.data?.message) + errorMessage += `${error.response.data.message}\n`; + if (error.response?.data?.errors[0]?.message) + errorMessage += `${error.response.data.errors[0].message}\n`; + } + + alert(`[Error] ${errorMessage}`); + }, + }); +}; diff --git a/apps/wr/src/shared/lib/use-prompt.tsx b/apps/wr/src/shared/lib/use-prompt.tsx new file mode 100644 index 000000000..f36858eb0 --- /dev/null +++ b/apps/wr/src/shared/lib/use-prompt.tsx @@ -0,0 +1,26 @@ +import { useContext } from "react"; +import { PromptContext } from "../providers/prompt-provider"; + +export const usePrompt = () => { + const { message, resolve, setMessage, setResolve } = + useContext(PromptContext); + + const prompt = (message: string) => { + setMessage(message); + return new Promise<{ resolved: boolean; value?: string }>((resolve) => { + setResolve(() => resolve); + }); + }; + + const onSubmit = (value: string) => { + resolve?.({ resolved: true, value }); + setMessage(""); + }; + + const onCancel = () => { + resolve?.({ resolved: false }); + setMessage(""); + }; + + return { message, isOpen: !!message, prompt, onSubmit, onCancel }; +}; diff --git a/apps/wr/src/shared/lib/use-user-store.ts b/apps/wr/src/shared/lib/use-user-store.ts new file mode 100644 index 000000000..748caed4c --- /dev/null +++ b/apps/wr/src/shared/lib/use-user-store.ts @@ -0,0 +1,14 @@ +import { useStore } from "zustand"; +import { useContext } from "react"; +import type { UserStore } from "../store/user-store"; +import { StoreContext } from "../providers/store-provider"; + +export const useUserStore = (selector: (store: UserStore) => T): T => { + const storeContext = useContext(StoreContext); + + if (!storeContext) { + throw new Error("useUserStore must be used within StoreProvider"); + } + + return useStore(storeContext.userStore, selector); +}; diff --git a/apps/wr/src/shared/providers/index.ts b/apps/wr/src/shared/providers/index.ts new file mode 100644 index 000000000..fa8320fe7 --- /dev/null +++ b/apps/wr/src/shared/providers/index.ts @@ -0,0 +1,2 @@ +export { StoreProvider } from "./store-provider"; +export { PromptProvider } from "./prompt-provider"; diff --git a/apps/wr/src/shared/providers/prompt-provider.tsx b/apps/wr/src/shared/providers/prompt-provider.tsx new file mode 100644 index 000000000..10338c338 --- /dev/null +++ b/apps/wr/src/shared/providers/prompt-provider.tsx @@ -0,0 +1,37 @@ +import { createContext, useState } from "react"; + +interface PromptContext { + message?: string; + resolve?: ({ + resolved, + value, + }: { + resolved: boolean; + value?: string; + }) => void; + setMessage: (message: string) => void; + setResolve: (resolve: () => void) => void; +} + +export const PromptContext = createContext({ + message: "", + resolve() {}, + setMessage() {}, + setResolve() {}, +}); + +export const PromptProvider = ({ children }: { children: React.ReactNode }) => { + const [message, setMessage] = useState(); + const [resolve, setResolve] = + useState< + ({ resolved, value }: { resolved: boolean; value?: string }) => void + >(); + + return ( + + {children} + + ); +}; diff --git a/apps/wr/src/shared/providers/store-provider.tsx b/apps/wr/src/shared/providers/store-provider.tsx new file mode 100644 index 000000000..a0ec17b61 --- /dev/null +++ b/apps/wr/src/shared/providers/store-provider.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { createContext, useRef } from "react"; +import { + createAlertStore, + createAttachmentStore, + createMenuStore, + createUserStore, +} from "../store"; + +type StoreContextType = { + alertStore: ReturnType; + attachmentStore: ReturnType; + menuStore: ReturnType; + userStore: ReturnType; +}; + +export const StoreContext = createContext( + undefined, +); + +export const StoreProvider = ({ children }: { children: React.ReactNode }) => { + const storeRef = useRef(null); + if (storeRef.current === null) { + storeRef.current = { + alertStore: createAlertStore(), + attachmentStore: createAttachmentStore(), + menuStore: createMenuStore(), + userStore: createUserStore(), + }; + } + + return ( + + {children} + + ); +}; diff --git a/apps/wr/src/shared/store/alert-store.ts b/apps/wr/src/shared/store/alert-store.ts new file mode 100644 index 000000000..3bba65848 --- /dev/null +++ b/apps/wr/src/shared/store/alert-store.ts @@ -0,0 +1,25 @@ +import { createStore } from "zustand/vanilla"; + +export type AlertState = { + message: string; + callback?: () => void; +}; + +export type AlertActions = { + setMessage: (message: string, callback?: () => void) => void; +}; + +export type AlertStore = AlertState & AlertActions; + +export const defaultInitState: AlertState = { + message: "", + callback: undefined, +}; + +export const createAlertStore = (initState: AlertState = defaultInitState) => { + return createStore()((set) => ({ + ...initState, + setMessage: (message: string, callback?: () => void) => + set(() => ({ message, callback })), + })); +}; diff --git a/apps/wr/src/shared/store/attachment-store.ts b/apps/wr/src/shared/store/attachment-store.ts new file mode 100644 index 000000000..faa700949 --- /dev/null +++ b/apps/wr/src/shared/store/attachment-store.ts @@ -0,0 +1,25 @@ +import { createStore } from "zustand/vanilla"; + +export type AttachmentState = { + attachmentFileCode: string; +}; + +export type AttachmentActions = { + setAttachmentFileCode: (attachmentFileCode: string) => void; +}; + +export type AttachmentStore = AttachmentState & AttachmentActions; + +export const defaultInitState: AttachmentState = { + attachmentFileCode: "", +}; + +export const createAttachmentStore = ( + initState: AttachmentState = defaultInitState, +) => { + return createStore()((set) => ({ + ...initState, + setAttachmentFileCode: (attachmentFileCode: string) => + set({ attachmentFileCode }), + })); +}; diff --git a/apps/wr/src/shared/store/index.ts b/apps/wr/src/shared/store/index.ts new file mode 100644 index 000000000..b0ca59c6c --- /dev/null +++ b/apps/wr/src/shared/store/index.ts @@ -0,0 +1,4 @@ +export { createAlertStore } from "./alert-store"; +export { createAttachmentStore } from "./attachment-store"; +export { createMenuStore, type BbsType } from "./menu-store"; +export { createUserStore, type IUserInfo } from "./user-store"; \ No newline at end of file diff --git a/apps/wr/src/shared/store/menu-store.ts b/apps/wr/src/shared/store/menu-store.ts new file mode 100644 index 000000000..8536011ee --- /dev/null +++ b/apps/wr/src/shared/store/menu-store.ts @@ -0,0 +1,93 @@ +import { createStore } from "zustand/vanilla"; + +export const BBS_TYPE = { + BBSNOTICE: "BBSNOTICE", + BBSNORMAL: "BBSNORMAL", + BBSFAQ: "BBSFAQ", + BBSIMAGE: "BBSIMAGE", + BBSMOVIE: "BBSMOVIE", +} as const; + +export type BbsType = (typeof BBS_TYPE)[keyof typeof BBS_TYPE]; + +export interface IBbs { + bbsId: number; + bbsTypeCd: BbsType; + bbsTypeNm: string; + bbsNm: string; + bbsExpln: string; + rfrncUseYn: string; + atflUseYn: string; + atflMaxCnt: number; + atflMaxFileSz: number; + ansUseYn: string; + ansUseAuthrt: string; + cmntUseYn: string; + dgstfnEvlUseYn: string; + smsUseYn: string; + upndFixUseYn: string; + pstgPrdUseYn: string; + popupUseYn: string; + delYn: string; + useYn: string; + mdfcnDt: string; + mdfrNm: string; +} + +interface IMenu { + menuCd: number; + menuKornNm: string; + menuEngNm: string; + parentId: number | null; + sortSeq: number; + iconNm?: string; + children: IMenu[] | null; + levelNo: number; + urlPath: string; + menuLnkgTypeCd: string; + menuLnkgSn: number; + menuPath: string; + qrcd: string; + useYn: string; + otptYn: string; + newWndwYn: string; + requiredUrlPath: boolean; +} + +interface IAuthority { + mngAuthrtYn: string; + inqAuthrtYn: string; + regAuthrtYn: string; + mdfcnAuthrtYn: string; + delAuthrtYn: string; +} + +export type MenuState = { + bbsInfo: IBbs | null; + currentMenuInfo: IMenu | null; + pageMenuAuthority: IAuthority | null; +}; + +export type MenuActions = { + setBbsInfo: (bbs: IBbs) => void; + setCurrentMenuInfo: (menu: IMenu) => void; + setPageMenuAuthority: (authority: IAuthority) => void; +}; + +export type MenuStore = MenuState & MenuActions; + +export const defaultInitState: MenuState = { + currentMenuInfo: null, + bbsInfo: null, + pageMenuAuthority: null, +}; + +export const createMenuStore = (initState: MenuState = defaultInitState) => { + return createStore()((set) => ({ + ...initState, + setBbsInfo: (bbs: IBbs) => set({ bbsInfo: bbs }), + setCurrentMenuInfo: (menu: IMenu) => set({ currentMenuInfo: menu }), + setPageMenuAuthority: (authority: IAuthority) => + set({ pageMenuAuthority: authority }), + })); +}; diff --git a/apps/wr/src/shared/store/user-store.ts b/apps/wr/src/shared/store/user-store.ts new file mode 100644 index 000000000..65d897cf1 --- /dev/null +++ b/apps/wr/src/shared/store/user-store.ts @@ -0,0 +1,57 @@ +import { createStore } from "zustand/vanilla"; + +export interface IUserInfo { + regDt: string; + mdfcnDt: string; + rgtr: string; + mdfr: string; + adminRoleYn: string; + userUnqId: string; + userId: string; + userName: string; + email: string; + encryptedPassword: string; + refreshToken: string; + userStateCode: string; + lastLoginDate: string; + loginFailCount: number; + pswdHnt: string; + pswdAns: string; + mphn: string; + telno: string; + zip: string; + addr: string; + daddr: string; + laeSeCd: string; + ogdpSeCd: string; + aprvYn: string; + autzrId: string; + expryYmd: string; + lastPswdChgDt: string; + googleId: string; + kakaoId: string; + naverId: string; + isSocialUser: boolean; + hasPassword: boolean; +} + +export type UserState = { + userInfo: IUserInfo | null; +}; + +export type UserActions = { + setUserInfo: (userInfo: IUserInfo | null) => void; +}; + +export type UserStore = UserState & UserActions; + +export const defaultInitState: UserState = { + userInfo: null, +}; + +export const createUserStore = (initState: UserState = defaultInitState) => { + return createStore()((set) => ({ + ...initState, + setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })), + })); +}; diff --git a/apps/wr/src/shared/ui/alert-dialog.tsx b/apps/wr/src/shared/ui/alert-dialog.tsx new file mode 100644 index 000000000..6160d5301 --- /dev/null +++ b/apps/wr/src/shared/ui/alert-dialog.tsx @@ -0,0 +1,34 @@ +import { + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Button, +} from "@common/components"; +import { useAlertStore } from "../lib/use-alert-store"; + +function AlertDialog() { + const { message, callback, setMessage } = useAlertStore((state) => state); + const handleOpenChange = () => { + setMessage(""); + callback?.(); + }; + return ( + + +
+

+ {message} +

+
+ + + + + +
+
+ ); +} + +export { AlertDialog }; diff --git a/apps/wr/src/shared/ui/confirm-dialog.tsx b/apps/wr/src/shared/ui/confirm-dialog.tsx new file mode 100644 index 000000000..6a4996bfb --- /dev/null +++ b/apps/wr/src/shared/ui/confirm-dialog.tsx @@ -0,0 +1,48 @@ +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Button, + type ButtonProps, +} from "@common/components"; + +type ConfirmDialogProps = { + isOpen?: boolean; + confirmButtonProps?: ButtonProps & { label?: string }; + cancelButtonProps?: ButtonProps & { label?: string }; +}; + +function ConfirmDialog({ + isOpen, + confirmButtonProps, + cancelButtonProps, +}: ConfirmDialogProps) { + const { message, isOpen: isGlobalOpen, onConfirm, onCancel } = useConfirm(); + return ( + + +
+

+ {message} +

+
+ + + + + + + + +
+
+ ); +} + +export { ConfirmDialog }; diff --git a/apps/wr/src/shared/ui/map-layout.tsx b/apps/wr/src/shared/ui/map-layout.tsx new file mode 100644 index 000000000..f743defbe --- /dev/null +++ b/apps/wr/src/shared/ui/map-layout.tsx @@ -0,0 +1,13 @@ +//import { useMenuStore } from "../lib/use-menu-store"; + +function MapLayout({ children }: { children: React.ReactNode }) { + //const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); + + return ( +
+ {children} +
+ ); +} + +export { MapLayout }; diff --git a/apps/wr/src/shared/ui/page-layout.tsx b/apps/wr/src/shared/ui/page-layout.tsx new file mode 100644 index 000000000..358d3b5db --- /dev/null +++ b/apps/wr/src/shared/ui/page-layout.tsx @@ -0,0 +1,22 @@ +import { PageTitle } from "@common/components"; +import { useMenuStore } from "../lib/use-menu-store"; + +function PageLayout({ children }: { children: React.ReactNode }) { + const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); + + const menuPaths = currentMenuInfo?.menuPath + ?.split(">") + .map((path) => path.trim()); + + return ( +
+ + {children} +
+ ); +} + +export { PageLayout }; diff --git a/apps/wr/src/shared/ui/prompt-dialog.tsx b/apps/wr/src/shared/ui/prompt-dialog.tsx new file mode 100644 index 000000000..496a18b5b --- /dev/null +++ b/apps/wr/src/shared/ui/prompt-dialog.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Input, +} from "@common/components"; +import { usePrompt } from "@/src/shared/lib/use-prompt"; + +function PromptDialog() { + const { message, isOpen, onSubmit, onCancel } = usePrompt(); + const [value, setValue] = useState(""); + + return ( + + +
+

+ {message} +

+ setValue(e.target.value)} + /> +
+ + + + + + + + +
+
+ ); +} + +export { PromptDialog };