diff --git a/.gitignore b/.gitignore
index bfdd87b0a..a355a3402 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,8 +4,9 @@
# dependencies
node_modules
+# build
+.next
+dist
+
# misc
.DS_Store
-
-# env
-.env*
\ No newline at end of file
diff --git a/apps/cs/.env.local b/apps/cs/.env.local
new file mode 100644
index 000000000..0648024ff
--- /dev/null
+++ b/apps/cs/.env.local
@@ -0,0 +1,2 @@
+NEXT_PUBLIC_PORT=3000
+NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000
diff --git a/apps/cs/.gitignore b/apps/cs/.gitignore
index 5ef6a5207..5340dd9a1 100644
--- a/apps/cs/.gitignore
+++ b/apps/cs/.gitignore
@@ -30,9 +30,6 @@ yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
-# env files (can opt-in for committing if needed)
-.env*
-
# vercel
.vercel
diff --git a/apps/cs/app/(with-base-layout)/layout.tsx b/apps/cs/app/(with-base-layout)/layout.tsx
new file mode 100644
index 000000000..cbcaee847
--- /dev/null
+++ b/apps/cs/app/(with-base-layout)/layout.tsx
@@ -0,0 +1,11 @@
+"use client";
+
+import { BaseLayout } from "@common/components/ui";
+
+export default function WithBaseLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return {children};
+}
diff --git a/apps/cs/app/site/page.tsx b/apps/cs/app/(with-base-layout)/site/page.tsx
similarity index 100%
rename from apps/cs/app/site/page.tsx
rename to apps/cs/app/(with-base-layout)/site/page.tsx
diff --git a/apps/cs/app/(with-base-layout)/site/register/[id]/page.tsx b/apps/cs/app/(with-base-layout)/site/register/[id]/page.tsx
new file mode 100644
index 000000000..685d77172
--- /dev/null
+++ b/apps/cs/app/(with-base-layout)/site/register/[id]/page.tsx
@@ -0,0 +1,10 @@
+import { ModifyPage as VModifyPage } from "@/pages/site";
+
+export default async function ModifyPage({
+ params,
+}: {
+ params: Promise<{ id: string }>;
+}) {
+ const { id } = await params;
+ return ;
+}
diff --git a/apps/cs/app/(with-base-layout)/site/register/page.tsx b/apps/cs/app/(with-base-layout)/site/register/page.tsx
new file mode 100644
index 000000000..01e25cc43
--- /dev/null
+++ b/apps/cs/app/(with-base-layout)/site/register/page.tsx
@@ -0,0 +1,2 @@
+import { CreatePage } from "@/pages/site";
+export default CreatePage;
diff --git a/apps/cs/app/api/proxy/[...url]/route.ts b/apps/cs/app/api/proxy/[...url]/route.ts
index 5397e5fe6..d3092c5c1 100644
--- a/apps/cs/app/api/proxy/[...url]/route.ts
+++ b/apps/cs/app/api/proxy/[...url]/route.ts
@@ -5,7 +5,7 @@ import {
REFRESH_TOKEN,
SERVER_API_URL,
SITE_ID,
-} from "@/shared/config/env";
+} from "@/shared/config";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
diff --git a/apps/cs/app/site/register/page.tsx b/apps/cs/app/site/register/page.tsx
deleted file mode 100644
index a11d0b60c..000000000
--- a/apps/cs/app/site/register/page.tsx
+++ /dev/null
@@ -1,2 +0,0 @@
-import { RegisterPage } from "@/pages/site";
-export default RegisterPage;
diff --git a/apps/cs/middleware.ts b/apps/cs/middleware.ts
new file mode 100644
index 000000000..e7b03dddc
--- /dev/null
+++ b/apps/cs/middleware.ts
@@ -0,0 +1,19 @@
+import { NextRequest, NextResponse } from "next/server";
+
+const serverApiUrl =
+ process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
+
+export function middleware(req: NextRequest) {
+ const pathname = req.nextUrl.pathname;
+ if (pathname.startsWith("/server")) {
+ return NextResponse.rewrite(
+ `${serverApiUrl}${pathname.replace("/server", "")}`,
+ );
+ }
+
+ return NextResponse.next();
+}
+
+export const config = {
+ matcher: "/server/:path*",
+};
diff --git a/apps/cs/next.config.ts b/apps/cs/next.config.ts
index fd2a3a156..7c1a12675 100644
--- a/apps/cs/next.config.ts
+++ b/apps/cs/next.config.ts
@@ -1,9 +1,6 @@
import type { NextConfig } from "next";
import webpack from "webpack";
-const serverApiUrl =
- process.env.NEXT_PUBLIC_SERVER_API_URL || "http://localhost:3000";
-
const nextConfig: NextConfig = {
webpack: (config) => {
config.plugins.push(
@@ -30,20 +27,11 @@ const nextConfig: NextConfig = {
"import.meta.env.VITE_AUTH_USER_ID": JSON.stringify(
process.env.NEXT_PUBLIC_AUTH_USER_ID,
),
- "process.env.SITE_ID": JSON.stringify(process.env.NEXT_PUBLIC_SITE_ID),
}),
);
return config;
},
- async rewrites() {
- return [
- {
- source: "/server/:path*",
- destination: `${serverApiUrl}/:path*`,
- },
- ];
- },
};
export default nextConfig;
diff --git a/apps/cs/package.json b/apps/cs/package.json
index 1797b4828..4b400a285 100644
--- a/apps/cs/package.json
+++ b/apps/cs/package.json
@@ -11,14 +11,22 @@
"dependencies": {
"@common/assets": "workspace:*",
"@common/components": "workspace:*",
+ "@frontend-opensource/use-react-hooks": "^1.1.4",
+ "@hookform/resolvers": "^4.1.3",
+ "@radix-ui/react-label": "^2.1.2",
+ "@radix-ui/react-slot": "^1.1.2",
"@tanstack/react-query": "^5.67.1",
"@tanstack/react-table": "^8.21.2",
"axios": "^1.8.1",
"chart.js": "^4.4.8",
+ "dayjs": "^1.11.13",
"js-cookie": "^3.0.5",
"next": "15.1.7",
"react-chartjs-2": "^5.3.0",
- "tailwindcss-animate": "^1.0.7"
+ "react-hook-form": "^7.54.2",
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^3.24.2",
+ "zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
diff --git a/apps/cs/src/app/index.tsx b/apps/cs/src/app/index.tsx
index 672023d15..d1d5e56e1 100644
--- a/apps/cs/src/app/index.tsx
+++ b/apps/cs/src/app/index.tsx
@@ -1,16 +1,12 @@
import "./styles/base.css";
import "./styles/components.css";
-import { AuthProvider, CookieProvider, QueryProvider } from "@/shared/provider";
+import { AppProvider } from "./providers";
const App = ({ children }: Readonly<{ children: React.ReactNode }>) => {
return (
-
-
- {children}
-
-
+ {children}
);
diff --git a/apps/cs/src/app/providers/app-provider.tsx b/apps/cs/src/app/providers/app-provider.tsx
new file mode 100644
index 000000000..cf477dcc0
--- /dev/null
+++ b/apps/cs/src/app/providers/app-provider.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
+import { AuthWrapper } from "@/entities/auth";
+import { QueryProvider, StoreProvider } from "@/shared/providers";
+
+const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+};
+
+export { AppProvider };
diff --git a/apps/cs/src/app/providers/index.ts b/apps/cs/src/app/providers/index.ts
new file mode 100644
index 000000000..15f99b7fd
--- /dev/null
+++ b/apps/cs/src/app/providers/index.ts
@@ -0,0 +1 @@
+export { AppProvider } from "./app-provider";
diff --git a/apps/cs/src/app/styles/components.css b/apps/cs/src/app/styles/components.css
index 4d5caaf96..ec137cf25 100644
--- a/apps/cs/src/app/styles/components.css
+++ b/apps/cs/src/app/styles/components.css
@@ -5,7 +5,7 @@
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
}
.card-border {
- @apply shadow-blur border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)];
+ @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;
@@ -13,4 +13,13 @@
.card-footer {
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
}
+
+ [data-mode="dark"] {
+ .card-border {
+ @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
+ }
+ .card-header {
+ @apply border-b-[#3f4042];
+ }
+ }
}
diff --git a/apps/cs/src/entities/auth/login/api/login-service.ts b/apps/cs/src/entities/auth/api/login-service.ts
similarity index 78%
rename from apps/cs/src/entities/auth/login/api/login-service.ts
rename to apps/cs/src/entities/auth/api/login-service.ts
index 722470184..c1968fe36 100644
--- a/apps/cs/src/entities/auth/login/api/login-service.ts
+++ b/apps/cs/src/entities/auth/api/login-service.ts
@@ -1,6 +1,11 @@
import { AxiosError } from "axios";
-import { ENDPOINT, JWT_EXPIRATION_TIME } from "@/shared/config/api";
-import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME } from "@/shared/config/env";
+import {
+ ENDPOINT,
+ JWT_EXPIRATION_TIME,
+ ACCESS_TOKEN,
+ AUTH_USER_ID,
+ CLAIM_NAME,
+} from "@/shared/config";
import type { LoginService, ILogin } from "../model/login-interface";
import { tokenManager } from "../lib/token-manager";
@@ -31,7 +36,7 @@ export class LoginApiService implements LoginService {
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
- throw new Error("[login] Missing required authentication tokens");
+ throw new Error("Missing required authentication tokens");
}
tokenManager.setToken(CLAIM_NAME, accessToken);
@@ -42,11 +47,9 @@ export class LoginApiService implements LoginService {
);
} catch (error) {
if (error instanceof AxiosError) {
- throw new Error(
- `Login failed: ${error.response?.data?.message || error.message}`,
- );
+ throw new Error(`Login failed: ${error.response?.data?.message}`);
}
- throw error;
+ throw new Error(`Login failed: ${error}`);
}
}
@@ -62,22 +65,19 @@ export class LoginApiService implements LoginService {
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
- throw new Error(
- "[silentRefresh] Missing required authentication tokens",
- );
+ throw new Error("Missing required authentication tokens");
}
tokenManager.setToken(CLAIM_NAME, accessToken);
tokenManager.setToken(AUTH_USER_ID, tokenId);
} catch (error) {
- console.warn("Token has expired, logging out.");
tokenManager.clearAuthToken();
if (error instanceof AxiosError) {
throw new Error(
- `Silent refresh failed: ${error.response?.data?.message || error.message}`,
+ `Silent refresh failed: ${error.response?.data?.message}`,
);
}
- throw error;
+ throw new Error(`Silent refresh failed: ${error}`);
}
}
}
diff --git a/apps/cs/src/entities/auth/index.ts b/apps/cs/src/entities/auth/index.ts
new file mode 100644
index 000000000..dc3fb96c9
--- /dev/null
+++ b/apps/cs/src/entities/auth/index.ts
@@ -0,0 +1,2 @@
+export { LoginApi } from "./api/login-service";
+export { AuthWrapper } from "./ui/auth-wrapper";
diff --git a/apps/cs/src/entities/auth/login/lib/token-manager.ts b/apps/cs/src/entities/auth/lib/token-manager.ts
similarity index 94%
rename from apps/cs/src/entities/auth/login/lib/token-manager.ts
rename to apps/cs/src/entities/auth/lib/token-manager.ts
index 4429c7d1b..0e761486a 100644
--- a/apps/cs/src/entities/auth/login/lib/token-manager.ts
+++ b/apps/cs/src/entities/auth/lib/token-manager.ts
@@ -1,5 +1,5 @@
-import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config/env";
-import { getCookie, setCookie, removeCookie } from "@/shared/lib/cookie";
+import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
+import { getCookie, setCookie, removeCookie } from "@/shared/lib";
export class TokenManager {
private static instance: TokenManager;
diff --git a/apps/cs/src/entities/auth/login/index.ts b/apps/cs/src/entities/auth/login/index.ts
deleted file mode 100644
index 8c7981ac9..000000000
--- a/apps/cs/src/entities/auth/login/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { LoginApi } from "./api/login-service";
diff --git a/apps/cs/src/entities/auth/login/model/login-interface.ts b/apps/cs/src/entities/auth/model/login-interface.ts
similarity index 100%
rename from apps/cs/src/entities/auth/login/model/login-interface.ts
rename to apps/cs/src/entities/auth/model/login-interface.ts
diff --git a/apps/cs/src/entities/auth/ui/auth-wrapper.tsx b/apps/cs/src/entities/auth/ui/auth-wrapper.tsx
new file mode 100644
index 000000000..ba175e697
--- /dev/null
+++ b/apps/cs/src/entities/auth/ui/auth-wrapper.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import { useQuery } from "@tanstack/react-query";
+import { client } from "@/shared/api";
+import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
+import { getCookie, useUserStore } from "@/shared/lib";
+import type { IUserInfo } from "@/shared/store";
+import { LoginApi } from "../api/login-service";
+
+const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
+ const router = useRouter();
+ const { setUserInfo } = useUserStore((state) => state);
+
+ useQuery({
+ queryKey: [ENDPOINT.USER_SERVICE.USERS],
+ queryFn: async () => {
+ try {
+ const userId = getCookie(AUTH_USER_ID);
+
+ if (!userId) {
+ throw new Error("Missing required authentication tokens");
+ }
+
+ const response = await client.get(
+ `${ENDPOINT.USER_SERVICE.USERS}/${userId}`,
+ );
+ setUserInfo(response.data);
+ return response;
+ } catch (error) {
+ console.error(error);
+ try {
+ await LoginApi.silentRefresh();
+ } catch (refreshError) {
+ console.error("Token refresh failed:", refreshError);
+ setUserInfo(null);
+ router.push("/auth/login");
+ }
+ }
+ },
+ });
+
+ return <>{children}>;
+};
+
+export { AuthWrapper };
diff --git a/apps/cs/src/entities/code/api/code-service.ts b/apps/cs/src/entities/code/api/code-service.ts
new file mode 100644
index 000000000..0d1055f6a
--- /dev/null
+++ b/apps/cs/src/entities/code/api/code-service.ts
@@ -0,0 +1,14 @@
+import { client } from "@/shared/api";
+import { ENDPOINT } from "@/shared/config";
+import type { CodeService, ICode } from "../model/code-interface";
+
+export class CodeApiService implements CodeService {
+ public async getCode(upCd: string): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.CODE}/${upCd}`,
+ );
+ return response.data;
+ }
+}
+
+export const CodeApi = new CodeApiService();
diff --git a/apps/cs/src/entities/code/index.ts b/apps/cs/src/entities/code/index.ts
new file mode 100644
index 000000000..0b6633f4b
--- /dev/null
+++ b/apps/cs/src/entities/code/index.ts
@@ -0,0 +1 @@
+export { CodeSelect } from "./ui/code-select";
diff --git a/apps/cs/src/entities/code/model/code-interface.ts b/apps/cs/src/entities/code/model/code-interface.ts
new file mode 100644
index 000000000..0d066e6da
--- /dev/null
+++ b/apps/cs/src/entities/code/model/code-interface.ts
@@ -0,0 +1,9 @@
+export interface ICode {
+ lwpoCd: string;
+ upCd: string;
+ lwpoCdNm: string;
+}
+
+export interface CodeService {
+ getCode(upCd: string): Promise;
+}
diff --git a/apps/cs/src/entities/code/ui/code-select.tsx b/apps/cs/src/entities/code/ui/code-select.tsx
new file mode 100644
index 000000000..00b3157aa
--- /dev/null
+++ b/apps/cs/src/entities/code/ui/code-select.tsx
@@ -0,0 +1,45 @@
+import { useQuery } from "@tanstack/react-query";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@common/components";
+import { ENDPOINT } from "@/shared/config";
+import { CodeApi } from "../api/code-service";
+
+type CodeSelectProps = {
+ upCd: string;
+ value?: string;
+ defaultValue?: string;
+ onValueChange?: (value: string) => void;
+};
+
+function CodeSelect({ upCd, defaultValue, ...props }: CodeSelectProps) {
+ const { data: code } = useQuery({
+ queryKey: [ENDPOINT.CMS_SERVICE.CODE, upCd],
+ queryFn: () => CodeApi.getCode(upCd),
+ enabled: !!upCd,
+ });
+
+ const defaultValueText =
+ code?.find((item) => item.lwpoCd === defaultValue)?.lwpoCdNm ?? "선택";
+
+ return (
+
+ );
+}
+
+export { CodeSelect };
diff --git a/apps/cs/src/entities/site/api/site-service.ts b/apps/cs/src/entities/site/api/site-service.ts
deleted file mode 100644
index 89ba82350..000000000
--- a/apps/cs/src/entities/site/api/site-service.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { client } from "@/shared/api/client";
-import { ENDPOINT } from "@/shared/config/api";
-import type {
- ISiteQuery,
- ISiteInfo,
- SiteService,
-} from "../model/site-interface";
-
-export class SiteApiService implements SiteService {
- public async siteInfo(query: ISiteQuery): Promise {
- const response = await client.get(
- `${ENDPOINT.CMS_SERVICE.SITES}`,
- { params: query },
- );
-
- return response.data;
- }
-}
-
-export const SiteApi = new SiteApiService();
diff --git a/apps/cs/src/entities/site/index.ts b/apps/cs/src/entities/site/index.ts
deleted file mode 100644
index 92ccd0ff7..000000000
--- a/apps/cs/src/entities/site/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { SiteApi } from "./api/site-service";
diff --git a/apps/cs/src/entities/site/model/site-interface.ts b/apps/cs/src/entities/site/model/site-interface.ts
deleted file mode 100644
index 8ba2da298..000000000
--- a/apps/cs/src/entities/site/model/site-interface.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-export interface ISiteQuery {
- siteNm: string;
- siteKndCd: string;
- useYn: string;
- page: number;
- size: number;
- sort: string[];
-}
-
-export interface ISite {
- siteId: string;
- siteNm: string;
- siteKindCode: string;
- bscSiteYn: string;
- useYn: string;
-}
-
-export interface ISiteInfo {
- totalPages: number;
- totalElements: number;
- number: number;
- sort: {
- empty: boolean;
- sorted: boolean;
- unsorted: boolean;
- };
- size: number;
- content: ISite[];
- pageable: {
- sort: {
- empty: boolean;
- sorted: boolean;
- unsorted: boolean;
- };
- offset: number;
- pageNumber: number;
- pageSize: number;
- paged: boolean;
- unpaged: boolean;
- };
- numberOfElements: number;
- first: boolean;
- last: boolean;
- empty: boolean;
-}
-
-export interface SiteService {
- siteInfo(query: ISiteQuery): Promise;
-}
diff --git a/apps/cs/src/entities/user/api/user-service.ts b/apps/cs/src/entities/user/api/user-service.ts
deleted file mode 100644
index 41319bc1f..000000000
--- a/apps/cs/src/entities/user/api/user-service.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { client } from "@/shared/api/client";
-import { ENDPOINT } from "@/shared/config/api";
-import type { IUser, IUserInfo, UserService } from "../model/user-interface";
-
-export class UsersApiService implements UserService {
- public async userInfo(payload: IUser): Promise {
- const response = await client.get(
- `${ENDPOINT.USER_SERVICE.USERS}/${payload.userUniqId}`,
- );
-
- return response.data;
- }
-}
-
-export const UserApi = new UsersApiService();
diff --git a/apps/cs/src/entities/user/index.ts b/apps/cs/src/entities/user/index.ts
deleted file mode 100644
index 3076aa261..000000000
--- a/apps/cs/src/entities/user/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { useUser } from "./lib/use-user";
diff --git a/apps/cs/src/entities/user/lib/use-user.ts b/apps/cs/src/entities/user/lib/use-user.ts
deleted file mode 100644
index 97cd2f034..000000000
--- a/apps/cs/src/entities/user/lib/use-user.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useQuery } from "@tanstack/react-query";
-import { ENDPOINT } from "@/shared/config/api";
-import { AUTH_USER_ID } from "@/shared/config/env";
-import { useCookie } from "@/shared/provider";
-import { UserApi } from "../api/user-service";
-
-const useUser = () => {
- const { getCookie } = useCookie();
- const { data } = useQuery({
- queryKey: [ENDPOINT.USER_SERVICE.USERS],
- queryFn: () =>
- UserApi.userInfo({
- userUniqId: getCookie(AUTH_USER_ID) ?? "",
- }),
- });
-
- return { userInfo: data };
-};
-
-export { useUser };
diff --git a/apps/cs/src/pages/auth/login/ui/login-page.tsx b/apps/cs/src/pages/auth/login/ui/login-page.tsx
index c8eb1e2f9..d5e12966a 100644
--- a/apps/cs/src/pages/auth/login/ui/login-page.tsx
+++ b/apps/cs/src/pages/auth/login/ui/login-page.tsx
@@ -2,7 +2,7 @@
import Image from "next/image";
import { images } from "@common/assets";
-import { LoginApi } from "@/entities/auth/login";
+import { LoginApi } from "@/entities/auth";
import "./login.css";
function LoginPage() {
diff --git a/apps/cs/src/pages/site/api/site-service.ts b/apps/cs/src/pages/site/api/site-service.ts
new file mode 100644
index 000000000..22363de68
--- /dev/null
+++ b/apps/cs/src/pages/site/api/site-service.ts
@@ -0,0 +1,54 @@
+import { client } from "@/shared/api";
+import { ENDPOINT } from "@/shared/config";
+import {
+ siteFormSchema,
+ type ISiteInfo,
+ type SiteService,
+ type ISiteDetail,
+ type ISiteParams,
+ type SiteFormData,
+} from "../model/site-interface";
+
+export class SiteApiService implements SiteService {
+ public async siteInfo(params: ISiteParams): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.SITES}`,
+ { params },
+ );
+ return response.data;
+ }
+
+ public async siteDetail(siteId: string): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
+ );
+ return response.data;
+ }
+
+ public async createSite(data: SiteFormData): Promise {
+ const validatedData = siteFormSchema.parse(data);
+ const response = await client.post(
+ `${ENDPOINT.CMS_SERVICE.SITES}`,
+ validatedData,
+ );
+ return response.data;
+ }
+
+ public async modifySite(siteId: string, data: SiteFormData): Promise {
+ const validatedData = siteFormSchema.parse(data);
+ const response = await client.put(
+ `${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
+ validatedData,
+ );
+ return response.data;
+ }
+
+ public async deleteSite(siteId: string): Promise {
+ const response = await client.delete(
+ `${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
+ );
+ return response.data;
+ }
+}
+
+export const SiteApi = new SiteApiService();
diff --git a/apps/cs/src/pages/site/index.ts b/apps/cs/src/pages/site/index.ts
index 22c7fef7b..40c65cf04 100644
--- a/apps/cs/src/pages/site/index.ts
+++ b/apps/cs/src/pages/site/index.ts
@@ -1,2 +1,3 @@
-export { RegisterPage } from "./ui/register-page";
+export { CreatePage } from "./ui/create-page";
+export { ModifyPage } from "./ui/modify-page";
export { SitePage } from "./ui/site-page";
diff --git a/apps/cs/src/pages/site/model/site-interface.ts b/apps/cs/src/pages/site/model/site-interface.ts
new file mode 100644
index 000000000..4d03e2c8b
--- /dev/null
+++ b/apps/cs/src/pages/site/model/site-interface.ts
@@ -0,0 +1,87 @@
+import { z } from "zod";
+
+export interface ISiteParams {
+ siteNm: string;
+ siteKndCd: string;
+ useYn: string;
+ page: number;
+ size: number;
+ sort: string[];
+}
+
+export interface ISite {
+ siteId: string;
+ siteNm: string;
+ siteKindCode: string;
+ bscSiteYn: string;
+ siteAddr: string;
+ useYn: string;
+ regDt: string;
+ rgTrNm: string;
+}
+
+export interface ISiteInfo {
+ totalPages: number;
+ totalElements: number;
+ number: number;
+ sort: {
+ empty: boolean;
+ sorted: boolean;
+ unsorted: boolean;
+ };
+ size: number;
+ content: ISite[];
+ pageable: {
+ sort: {
+ empty: boolean;
+ sorted: boolean;
+ unsorted: boolean;
+ };
+ offset: number;
+ pageNumber: number;
+ pageSize: number;
+ paged: boolean;
+ unpaged: boolean;
+ };
+ numberOfElements: number;
+ first: boolean;
+ last: boolean;
+ empty: boolean;
+}
+
+export interface ISiteDetail extends SiteFormData {
+ siteId: string;
+ siteKindNm: string;
+ basicSiteYn: string;
+ faxNumber: string;
+ regDt: string;
+ rgTr: string;
+ mdfcnDt: string;
+ mdFr: string;
+ rgTrNm: string;
+ mdFrNm: string;
+}
+
+export const siteFormSchema = z.object({
+ siteId: z.string().max(2).nonempty(),
+ siteNm: z.string().max(100).nonempty(),
+ siteExpln: z.string().max(1000).optional(),
+ siteKndCd: z.string().max(10).nonempty(),
+ siteAddr: z.string().max(2000).optional(),
+ faxNo: z.string().max(100).optional(),
+ telNo: z.string().max(1000).optional(),
+ lwndCn: z.string().max(2000).optional(),
+ bscSiteYn: z.string(),
+ bkmkIcon: z.string().max(100).optional(),
+ useYn: z.string(),
+ siteSkn: z.string().max(10).optional(),
+});
+
+export type SiteFormData = z.infer;
+
+export interface SiteService {
+ siteInfo(query: ISiteParams): Promise;
+ siteDetail(siteId: string): Promise;
+ modifySite(siteId: string, data: SiteFormData): Promise;
+ createSite(data: SiteFormData): Promise;
+}
diff --git a/apps/cs/src/pages/site/model/__mocks__.tsx b/apps/cs/src/pages/site/model/table-columns.tsx
similarity index 69%
rename from apps/cs/src/pages/site/model/__mocks__.tsx
rename to apps/cs/src/pages/site/model/table-columns.tsx
index 2b55da7d8..35c084896 100644
--- a/apps/cs/src/pages/site/model/__mocks__.tsx
+++ b/apps/cs/src/pages/site/model/table-columns.tsx
@@ -1,5 +1,6 @@
+import dayjs from "dayjs";
import { createColumnHelper } from "@tanstack/react-table";
-import type { ISite } from "@/entities/site/model/site-interface";
+import type { ISite } from "./site-interface";
const columnHelper = createColumnHelper();
@@ -16,8 +17,8 @@ const columns = [
id: "siteNm",
header: "사이트명",
}),
- columnHelper.accessor("siteKindCode", {
- id: "unknown1",
+ columnHelper.accessor("siteAddr", {
+ id: "siteAddr",
header: "대표 URL",
}),
columnHelper.accessor("bscSiteYn", {
@@ -30,13 +31,17 @@ const columns = [
header: "사용 여부",
cell: (info) => (info.getValue() ? "Y" : "N"),
}),
- columnHelper.accessor("siteId", {
- id: "unknown2",
+ columnHelper.accessor("rgTrNm", {
+ id: "rgTrNm",
header: "등록자",
}),
- columnHelper.accessor("siteId", {
- id: "ununknown3",
+ columnHelper.accessor("regDt", {
+ id: "regDt",
header: "등록일",
+ cell: (info) => {
+ const date = dayjs(info.getValue());
+ return date.format("YYYY-MM-DD");
+ },
}),
];
diff --git a/apps/cs/src/pages/site/ui/create-page.tsx b/apps/cs/src/pages/site/ui/create-page.tsx
new file mode 100644
index 000000000..a79c16022
--- /dev/null
+++ b/apps/cs/src/pages/site/ui/create-page.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { useForm } from "react-hook-form";
+import { useMutation } from "@tanstack/react-query";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { SiteApi } from "../api/site-service";
+import { siteFormSchema, type SiteFormData } from "../model/site-interface";
+import { SitePageLayout } from "./layout";
+import { SiteForm } from "./site-form";
+import {
+ Button,
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogFooter,
+} from "@common/components/ui";
+
+function CreatePage() {
+ const router = useRouter();
+ const { mutateAsync: createSite } = useMutation({
+ mutationFn: (data: SiteFormData) => SiteApi.createSite(data),
+ onSuccess: () => {
+ setIsCreateSuccess(true);
+ },
+ });
+
+ const [isCreateSuccess, setIsCreateSuccess] = useState(false);
+
+ const form = useForm({
+ resolver: zodResolver(siteFormSchema),
+ });
+
+ return (
+
+
+
+
+ );
+}
+
+export { CreatePage };
diff --git a/apps/cs/src/pages/site/ui/domain-management-dialog.tsx b/apps/cs/src/pages/site/ui/domain-management-dialog.tsx
new file mode 100644
index 000000000..c675f92d9
--- /dev/null
+++ b/apps/cs/src/pages/site/ui/domain-management-dialog.tsx
@@ -0,0 +1,210 @@
+import { useState } from "react";
+import {
+ Dialog,
+ DialogTrigger,
+ Button,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+ Input,
+ Checkbox,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+ DialogFooter,
+ DialogClose,
+} from "@common/components";
+
+export type Domain = {
+ id: number;
+ domainUrl: string;
+ isRepresentative: boolean;
+ isActive: "true" | "false";
+};
+
+type DomainManagementDialogProps = {
+ triggerDisabled?: boolean;
+ handleSave: (rows: Domain[]) => void;
+};
+
+function DomainManagementDialog({
+ triggerDisabled,
+ handleSave,
+}: DomainManagementDialogProps) {
+ const [rows, setRows] = useState([]);
+ const [rowSelection, setRowSelection] = useState([]);
+
+ const onSave = () => {
+ handleSave(rows);
+ };
+
+ const handleAddDomain = () => {
+ const newDomain: Domain = {
+ id: Date.now(),
+ domainUrl: "",
+ isRepresentative: rows.length === 0,
+ isActive: "true",
+ };
+ setRows([...rows, newDomain]);
+ };
+
+ const handleDeleteDomain = () => {
+ const filteredRows = rows.filter((row) => !rowSelection.includes(row.id));
+ const hasRepresentative = filteredRows.some((row) => row.isRepresentative);
+
+ let updatedRows = filteredRows;
+ if (!hasRepresentative && filteredRows.length > 0) {
+ updatedRows = filteredRows.map((row, index) =>
+ index === 0 ? { ...row, isRepresentative: true } : row,
+ );
+ }
+
+ setRows(updatedRows);
+ setRowSelection([]);
+ };
+
+ const handleDomainUrlChange = (id: number, value: string) => {
+ setRows(
+ rows.map((row) => (row.id === id ? { ...row, domainUrl: value } : row)),
+ );
+ };
+
+ const handleRepresentativeChange = (id: number) => {
+ setRows(
+ rows.map((row) => ({
+ ...row,
+ isRepresentative: row.id === id,
+ })),
+ );
+ };
+
+ const handleActiveChange = (id: number, value: "true" | "false") => {
+ setRows(
+ rows.map((row) => (row.id === id ? { ...row, isActive: value } : row)),
+ );
+ };
+
+ const toggleRowSelection = (id: number) => {
+ setRowSelection((prev) => {
+ if (prev.includes(id)) {
+ return prev.filter((item) => item !== id);
+ } else {
+ return [...prev, id];
+ }
+ });
+ };
+
+ return (
+
+ );
+}
+
+export { DomainManagementDialog };
diff --git a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx
index 04b71afc7..06d9638b3 100644
--- a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx
+++ b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx
@@ -1,5 +1,3 @@
-"use client";
-
import { useState } from "react";
import {
Dialog,
@@ -18,6 +16,8 @@ import {
RadioGroup,
RadioGroupItem,
Checkbox,
+ DialogFooter,
+ DialogClose,
} from "@common/components";
type IP = {
@@ -26,11 +26,15 @@ type IP = {
allowed: boolean;
};
-function IPManagementDialog() {
+type IPManagementDialogProps = {
+ triggerDisabled?: boolean;
+};
+
+function IPManagementDialog({ triggerDisabled }: IPManagementDialogProps) {
const [rows, setRows] = useState([]);
const [rowSelection, setRowSelection] = useState([]);
- const handleSave = () => {};
+ const onSave = () => {};
const handleAddIP = () => {
const newIP: IP = {
@@ -72,9 +76,9 @@ function IPManagementDialog() {
return (