sn: 로그인 페이지 추가 및 남은시간

This commit is contained in:
2026-04-17 15:01:56 +09:00
parent 75eaaa81ac
commit f88b4dfc7e
21 changed files with 1106 additions and 10 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083/sewerage-service
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
# NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
#NEXT_PUBLIC_SERVER_API_URL = http://localhost:8883/operation-service
+1 -1
View File
@@ -16,4 +16,4 @@ NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com/dc-service
NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083/sewerage-service
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
+2
View File
@@ -0,0 +1,2 @@
import { LoginPage } from "@/src/features/auth/login";
export default LoginPage;
@@ -1,4 +1,3 @@
import { tokenManager } from "@/src/entities/auth/lib/token-manager";
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
@@ -6,7 +5,6 @@ export async function GET(request: NextRequest) {
const redirectUrl = process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "https://portal.hasuon.or.kr";
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "con.mind-one.co.kr") {
tokenManager.clearAuthToken();
return NextResponse.redirect(new URL("/", request.url)); // 로컬 및 개발 환경
} else {
return NextResponse.redirect(new URL(redirectUrl)); // 공단 환경
+39 -1
View File
@@ -1,6 +1,29 @@
import { SITE_ID } from "@/src/shared/config/env";
import { CLAIM_NAME, SITE_ID } from "@/src/shared/config/env";
import { tokenManager } from "@/src/entities/auth/lib/token-manager";
import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "./config/env";
const LOGIN_PAGE_URL =
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
const handleUnauthorizedResponse = () => {
if (typeof window === "undefined") return;
const remainingMs = tokenManager.getRemainingMsFromToken(CLAIM_NAME);
const isExpired = remainingMs === null || remainingMs <= 0;
if (!isExpired) return;
tokenManager.clearAuthToken();
const isLoginPage =
window.location.pathname === "/auth/login" ||
window.location.pathname.startsWith("/auth/login/");
if (!isLoginPage) {
window.location.assign(LOGIN_PAGE_URL);
}
};
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;
@@ -11,6 +34,9 @@ export const getFetch = async (part : string, url: string) => {
"X-Site-Id": SITE_ID,
}
});
if (response.status === 401) {
handleUnauthorizedResponse();
}
if(!response.ok) {
throw new Error(`오류 발생: ${response.status}`);
}
@@ -26,6 +52,9 @@ export const getFetchAreaList = async (url: string) => {
"X-Site-Id": SITE_ID,
}
});
if (response.status === 401) {
handleUnauthorizedResponse();
}
if(!response.ok) {
throw new Error(`오류 발생: ${response.status}`);
}
@@ -44,6 +73,9 @@ export const postFetch = async (part : string, url: string, params: unknown) =>
},
body: JSON.stringify(params),
});
if (response.status === 401) {
handleUnauthorizedResponse();
}
if(!response.ok) {
throw new Error(`오류 발생: ${response.status}`);
}
@@ -61,6 +93,9 @@ export const putFetch = async (part : string, url: string, params: unknown) => {
},
body: JSON.stringify(params),
});
if (response.status === 401) {
handleUnauthorizedResponse();
}
if(!response.ok) {
throw new Error(`오류 발생: ${response.status}`);
}
@@ -78,6 +113,9 @@ export const deleteFetch = async (part : string, url: string, params: unknown) =
},
body: JSON.stringify(params),
});
if (response.status === 401) {
handleUnauthorizedResponse();
}
if(!response.ok) {
throw new Error(`오류 발생: ${response.status}`);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

@@ -1,6 +1,13 @@
import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config";
import { getCookie, removeCookie, setCookie } from "@/src/shared/lib/cookie";
type JwtPayload = {
sub?: string;
exp?: number;
iat?: number;
[key: string]: unknown;
};
export class TokenManager {
private static instance: TokenManager;
private refreshTimer: NodeJS.Timeout | null = null;
@@ -18,6 +25,49 @@ export class TokenManager {
return getCookie(key);
}
private parseJwt(token: string): JwtPayload {
const parts = token.split(".");
if (parts.length !== 3) {
throw new Error("유효한 JWT 형식이 아닙니다.");
}
const base64Url = parts[1] ?? "";
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
const jsonPayload = decodeURIComponent(
atob(padded)
.split("")
.map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`)
.join("")
);
return JSON.parse(jsonPayload) as JwtPayload;
}
public getRemainingMsFromToken(key: string = CLAIM_NAME): number | null {
const token = this.getToken(key);
if (!token) {
return null;
}
try {
const payload = this.parseJwt(token);
if (typeof payload.exp !== "number") {
return null;
}
return payload.exp * 1000 - Date.now();
} catch (error) {
console.error("JWT 파싱 오류:", error);
return null;
}
}
public isExpiredToken(key: string = CLAIM_NAME): boolean {
const remainingMs = this.getRemainingMsFromToken(key);
return remainingMs === null || remainingMs <= 0;
}
public setToken(key: string, value: string): void {
setCookie(key, value, {
secure: location.protocol === "https:",
+1
View File
@@ -0,0 +1 @@
export { LoginPage } from "./ui/login-page";
@@ -0,0 +1,139 @@
"use client";
import Image from "next/image";
import { useEffect } from "react";
import { images } from "@common/assets";
import { LoginApi } from "@/src/entities/auth";
import "./login.css";
function LoginPage() {
useEffect(() => {
const hostname = window.location.hostname;
if (hostname.includes("hasuon.or.kr")) {
window.location.href =
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
}
}, []);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const userId = formData.get("id") as string;
const password = formData.get("password") as string;
await LoginApi.login({ userId, password });
window.location.href = "/pages/dashboard";
};
return (
<div className="login fx">
<div className="left_box fx">
<div className="text">
<h1>
하수도정보시스템
<br />
<span>CMS 통합관리시스템</span>
</h1>
<p>하수도정보시스템 CMS 통합관리시스템에 오신 것을 환영합니다.</p>
</div>
<div className="img">
<Image src={images.logo} width={229} height={46} alt="logo" />
</div>
</div>
<form
id="loginForm"
name="loginForm"
method="post"
onSubmit={handleSubmit}
>
<div className="login_box fx">
<ul className="fx">
<li>
<h2>ID</h2>
<div className="input_st st1">
<i className="diveicon di-account-box" />
<input
type="text"
className="id"
title="아이디를 입력하세요."
id="id"
name="id"
maxLength={12}
/>
</div>
</li>
<li>
<h2>PASSWORD</h2>
<div className="input_st st2">
<i className="diveicon di-key" />
<input
type="password"
className="input_style"
maxLength={25}
title="비밀번호를 입력하세요."
id="password"
name="password"
/>
</div>
</li>
</ul>
<div className="bottom_box fx">
<div className="checks fx">
<input
type="checkbox"
name="checkId"
title="로그인ID 저장여부"
id="checkId"
/>
<label htmlFor="checkId">아이디저장</label>
</div>
{/* <a href="javascript:fnUserSearch();" className=" pw_message fx"><i className="xi-external-link"></i>비밀번호 또는 아이디를 잃어버리셨나요?</a> */}
</div>
<button type="submit" className="login_btn fx">
<span>LOGIN</span>
</button>
<div className="footer">
<div className="copy fx">
<p>copyright © 2023 KECO. All rights reserved.</p>
</div>
<div className="fot_btn fx">
<a
href="https://keco.or.kr/web/lay1/S1T6C1348/contents.do"
target="_blank"
>
개인정보처리방침
</a>
<a
href="https://keco.or.kr/web/lay1/S1T6C1349/contents.do"
target="_blank"
>
저작권정책안내
</a>
<a
href="https://keco.or.kr/web/lay1/S1T6C1350/contents.do"
target="_blank"
>
이메일무단수집거부
</a>
</div>
<p className="addr fx">
<span>
주소 : (22689) 인천광역시 서구 환경로 42(경서동
종합환경연구단지)
</span>
<span>
전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계) 032-590-3723,
3722
</span>
<span>팩스 :032-590-3729</span>
</p>
</div>
</div>
</form>
</div>
);
}
export { LoginPage };
@@ -0,0 +1,310 @@
/**
* OLYM Cascading Style Sheets
* File Name : login.css
* Description : 로그인 스타일시트
* Author : Web business Team / Choi Hyun Mi
* Email : desingtj@olym.co.kr
* Date : 2023.07.20
* Update : 2023.07.26
* Copyright(c) 2023 OLYM Communications. All Rights Reserved.
*/
html,
body {
background: #fff;
}
.fx {
display: flex;
-ms-display: flex;
-webkit-display: flex;
box-sizing: border-box;
}
.login {
width: 100%;
height: 100vh;
}
.login .left_box {
width: 100%;
background: url(/images/bg_login.jpg) no-repeat center/cover;
flex-direction: column;
justify-content: space-between;
padding: 7% 5% 5% 5%;
}
.login .left_box .text h1 {
font-size: 53px;
color: #fff;
}
.login .left_box .text h1 span {
font-weight: 800;
}
.login .left_box .text p {
margin-top: 40px;
font-size: 18px;
color: #fff;
}
.login .login_box {
width: 100%;
flex-direction: column;
justify-content: center;
padding: 4% 12% 4% 6%;
}
.login .login_box > ul {
flex-direction: column;
gap: 40px;
}
.login .login_box > ul > li h2 {
font-size: 22px;
font-weight: 600;
color: #2640a5;
margin-bottom: 14px;
}
.login .login_box > ul > li .input_st {
position: relative;
}
.login .login_box > ul > li .input_st .diveicon {
position: absolute;
top: 50%;
left: 28px;
transform: translateY(-50%);
font-family: "Xeicon";
font-size: 25px;
color: #003288;
}
.login .login_box > ul > li input {
width: 100%;
padding: 20px 20px 20px 70px;
background: #f7f7f7;
box-sizing: border-box;
border: none;
border-radius: 5px;
font-size: 18px;
font-weight: 700;
color: #000;
height: auto;
}
.login .login_box > ul > li input:focus {
outline: 1px solid #3c5fe5;
}
.login .login_box > ul > li input::placeholder {
color: #afabab;
font-weight: 500;
}
.login .login_box .bottom_box {
justify-content: space-between;
gap: 20px;
padding: 50px 0 45px 0;
}
.login .login_box .bottom_box .checks {
display: flex;
align-items: center;
gap: 8px;
}
.login .login_box .bottom_box .checks input[type="checkbox"] {
width: 18px;
height: 18px;
}
.login .login_box .bottom_box .checks label {
font-size: 14px;
}
.login .login_box .bottom_box .pw_message {
align-items: center;
gap: 5px;
color: #636363;
font-weight: 500;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
}
.login .login_box .bottom_box .pw_message i {
color: #327af5;
font-size: 20px;
}
.login .login_box .login_btn {
position: relative;
border-radius: 5px;
padding: 24px 15px;
background: linear-gradient(
to left,
#1d3969 0%,
#264078 10%,
#172558 40%,
#162254 50%,
#111c43 60%,
#111c42 100%
);
overflow: hidden;
justify-content: center;
align-items: center;
gap: 10px;
color: #fff;
cursor: pointer;
}
.login .login_box .login_btn span {
font-family: "Montserrat";
font-size: 19px;
font-weight: 700;
position: relative;
}
.login .login_box .login_btn i {
font-size: 22px;
transform: translateY(-2px);
}
.login .login_box .login_btn:before {
display: block;
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
background: linear-gradient(
to right,
#1d3969 0%,
#264078 10%,
#172558 40%,
#162254 50%,
#111c43 60%,
#111c42 100%
);
transition: opacity 0.5s;
}
.login .login_box .login_btn:hover::before {
opacity: 1;
}
.login .footer .copy {
margin-top: 36px;
justify-content: space-between;
align-items: center;
gap: 3%;
}
.login .footer .copy p {
font-size: 13px;
color: #d4d4d4;
width: 44%;
text-align: center;
}
.login .footer .copy::before,
.login .login_box .copy::after {
content: "";
width: 20%;
height: 1px;
background: #d4d4d4;
}
.login .footer .fot_btn {
gap: 20px;
margin: 30px 0 10px 0;
}
.login .footer .fot_btn a {
color: #333;
font-weight: 700;
font-size: 13px;
}
.login .footer .fot_btn a:hover {
color: #003288;
}
.login .footer .addr {
color: #666;
font-size: 13px;
gap: 3px 20px;
flex-wrap: wrap;
}
/* 비밀번호 변경 팝업창 */
.login .modal .modal_container {
width: 750px;
}
.login .pw_desc {
padding: 10px 10px 30px 10px;
color: #666;
}
.login .pw_desc p {
font-size: 1.25rem;
color: #000;
font-weight: 700;
margin-bottom: 10px;
}
.login .pw_desc span {
color: #f60;
font-weight: 700;
}
.login .modal .form_write {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
padding: 20px 50px;
}
@media (max-width: 1024px) {
.login {
flex-direction: column;
height: auto;
}
.login .left_box {
padding: 30px;
flex-direction: column-reverse;
gap: 20px;
}
.login .left_box .text h1 {
font-size: 30px;
line-height: 1.2em;
}
.login .left_box .text p {
margin-top: 20px;
font-size: 14px;
}
.login .left_box .img img {
height: 40px;
}
.login .login_box {
padding: 30px;
}
.login .login_box > ul {
gap: 20px;
}
.login .login_box > ul > li h2 {
margin-bottom: 7px;
}
.login .login_box > ul > li .input_st::after {
left: 15px;
font-size: 20px;
}
.login .login_box > ul > li input {
padding: 15px 10px 15px 50px;
font-size: 16px;
}
.login .login_box .bottom_box {
flex-direction: column;
align-items: flex-start;
padding: 30px 0;
}
.login .login_box .bottom_box .id_save label {
font-size: 14px;
}
.login .login_box .login_btn {
padding: 15px;
}
.login .login_box .login_btn span {
font-size: 16px;
}
.login .login_box .login_btn i {
font-size: 18px;
transform: translateY(-1px);
}
.login .login_box .copy p {
width: 100%;
}
.login .login_box .copy::before,
.login .login_box .copy::after {
display: none;
}
.login .footer .addr {
font-size: 11px;
gap: 3px 10px;
}
.login .modal .modal_container {
width: 90%;
}
.login .pw_desc {
padding: 0 0 20px 0;
}
.login .modal .form_write {
padding: 10px;
}
}
/* // login */
+1 -1
View File
@@ -15,5 +15,5 @@ NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
# NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883
+1 -1
View File
@@ -13,4 +13,4 @@ NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
+2
View File
@@ -0,0 +1,2 @@
import { LoginPage } from "@/features/auth/login";
export default LoginPage;
+50 -1
View File
@@ -11,12 +11,15 @@
*/
import { useEffect, useState } from "react";
import axios from 'axios';
import { SITE_ID } from "@/shared/config";
import { CLAIM_NAME, SITE_ID } from "@/shared/config";
import { tokenManager } from "@/entities/auth/lib/token-manager";
const BASE_URL = '/sewerage-service/api';
const DEFAULT_HEADERS = {
"X-Site-Id": SITE_ID,
};
const LOGIN_PAGE_URL =
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
const withSiteIdHeaders = (headers: HeadersInit = {}) => ({
...headers,
@@ -26,11 +29,33 @@ const withSiteIdHeaders = (headers: HeadersInit = {}) => ({
const resolveApiUrl = (url: string) =>
url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`;
const handleUnauthorizedResponse = () => {
if (typeof window === "undefined") return;
const remainingMs = tokenManager.getRemainingMsFromToken(CLAIM_NAME);
const isExpired = remainingMs === null || remainingMs <= 0;
if (!isExpired) return;
tokenManager.clearAuthToken();
const isLoginPage =
window.location.pathname === "/auth/login" ||
window.location.pathname.startsWith("/auth/login/");
if (!isLoginPage) {
window.location.assign(LOGIN_PAGE_URL);
}
};
export const getData = async (url: string): Promise<any> => {
const res = await fetch(resolveApiUrl(url), {
method: "GET",
headers: withSiteIdHeaders(),
});
if (res.status === 401) {
handleUnauthorizedResponse();
}
if (res.ok) {
try {
// 응답이 비어있는지 확인
@@ -66,6 +91,9 @@ export const postData = async (url: string, data: object): Promise<any> => {
body: JSON.stringify(data),
};
const res = await fetch(`${BASE_URL}/${url}`, options);
if (res.status === 401) {
handleUnauthorizedResponse();
}
if (res.ok) {
const resData = await res.json();
console.log('postData response:', resData);
@@ -91,6 +119,9 @@ export const putData = async (url: string, data: object): Promise<any> => {
body: JSON.stringify(data),
};
const res = await fetch(`${BASE_URL}/${url}`, options);
if (res.status === 401) {
handleUnauthorizedResponse();
}
const resData = await res.json();
console.log('putData response:', resData);
return resData.data;
@@ -105,6 +136,9 @@ export const deleteData = async (url: string, data?: object): Promise<any> => {
body: data ? JSON.stringify(data) : undefined,
};
const res = await fetch(resolveApiUrl(url), options);
if (res.status === 401) {
handleUnauthorizedResponse();
}
if (res.status === 204) return true
return res;
};
@@ -114,6 +148,9 @@ export const getRawJson = async (url: string): Promise<any> => {
method: "GET",
headers: withSiteIdHeaders(),
});
if (res.status === 401) {
handleUnauthorizedResponse();
}
if (!res.ok) {
throw new Error('Failed to fetch data');
@@ -142,6 +179,9 @@ export const getBlobData = async (url: string): Promise<Blob> => {
method: "GET",
headers: withSiteIdHeaders(),
});
if (res.status === 401) {
handleUnauthorizedResponse();
}
if (!res.ok) {
throw new Error('Failed to fetch data');
@@ -156,6 +196,9 @@ export const getDcData = async (url: string, resType: string = "json"): Promise<
method: "GET",
headers: withSiteIdHeaders(),
});
if (res.status === 401) {
handleUnauthorizedResponse();
}
if (res.ok) {
let data;
if (resType === "json") data = await res.json();
@@ -172,6 +215,9 @@ export const getCmsData = async (url: string, data?: object): Promise<any> => {
method: "GET",
headers: withSiteIdHeaders(),
});
if (res.status === 401) {
handleUnauthorizedResponse();
}
if (res.ok) {
const data = await res.json();
return data;
@@ -185,6 +231,9 @@ export const getGeoData = async (url: string, data?: object): Promise<any> => {
method: "GET",
headers: withSiteIdHeaders(),
});
if (res.status === 401) {
handleUnauthorizedResponse();
}
if (res.ok) {
const data = await res.json();
return data;
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

@@ -1,6 +1,13 @@
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
import { getCookie, setCookie, removeCookie } from "@/shared/lib";;
type JwtPayload = {
sub?: string;
exp?: number;
iat?: number;
[key: string]: unknown;
};
export class TokenManager {
private static instance: TokenManager;
private refreshTimer: NodeJS.Timeout | null = null;
@@ -18,6 +25,49 @@ export class TokenManager {
return getCookie(key);
}
private parseJwt(token: string): JwtPayload {
const parts = token.split(".");
if (parts.length !== 3) {
throw new Error("유효한 JWT 형식이 아닙니다.");
}
const base64Url = parts[1] ?? "";
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
const jsonPayload = decodeURIComponent(
atob(padded)
.split("")
.map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`)
.join("")
);
return JSON.parse(jsonPayload) as JwtPayload;
}
public getRemainingMsFromToken(key: string = CLAIM_NAME): number | null {
const token = this.getToken(key);
if (!token) {
return null;
}
try {
const payload = this.parseJwt(token);
if (typeof payload.exp !== "number") {
return null;
}
return payload.exp * 1000 - Date.now();
} catch (error) {
console.error("JWT 파싱 오류:", error);
return null;
}
}
public isExpiredToken(key: string = CLAIM_NAME): boolean {
const remainingMs = this.getRemainingMsFromToken(key);
return remainingMs === null || remainingMs <= 0;
}
public setToken(key: string, value: string): void {
// 환경에 따른 도메인 설정
//const domain = this.getCookieDomain();
+1
View File
@@ -0,0 +1 @@
export { LoginPage } from "./ui/login-page";
@@ -0,0 +1,139 @@
"use client";
import Image from "next/image";
import { useEffect } from "react";
import { images } from "@common/assets";
import { LoginApi } from "@/entities/auth";
import "./login.css";
function LoginPage() {
useEffect(() => {
const hostname = window.location.hostname;
if (hostname.includes("hasuon.or.kr")) {
window.location.href =
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
}
}, []);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const userId = formData.get("id") as string;
const password = formData.get("password") as string;
await LoginApi.login({ userId, password });
window.location.href = "/map";
};
return (
<div className="login fx">
<div className="left_box fx">
<div className="text">
<h1>
하수도정보시스템
<br />
<span>CMS 통합관리시스템</span>
</h1>
<p>하수도정보시스템 CMS 통합관리시스템에 오신 것을 환영합니다.</p>
</div>
<div className="img">
<Image src={images.logo} width={229} height={46} alt="logo" />
</div>
</div>
<form
id="loginForm"
name="loginForm"
method="post"
onSubmit={handleSubmit}
>
<div className="login_box fx">
<ul className="fx">
<li>
<h2>ID</h2>
<div className="input_st st1">
<i className="diveicon di-account-box" />
<input
type="text"
className="id"
title="아이디를 입력하세요."
id="id"
name="id"
maxLength={12}
/>
</div>
</li>
<li>
<h2>PASSWORD</h2>
<div className="input_st st2">
<i className="diveicon di-key" />
<input
type="password"
className="input_style"
maxLength={25}
title="비밀번호를 입력하세요."
id="password"
name="password"
/>
</div>
</li>
</ul>
<div className="bottom_box fx">
<div className="checks fx">
<input
type="checkbox"
name="checkId"
title="로그인ID 저장여부"
id="checkId"
/>
<label htmlFor="checkId">아이디저장</label>
</div>
{/* <a href="javascript:fnUserSearch();" className=" pw_message fx"><i className="xi-external-link"></i>비밀번호 또는 아이디를 잃어버리셨나요?</a> */}
</div>
<button type="submit" className="login_btn fx">
<span>LOGIN</span>
</button>
<div className="footer">
<div className="copy fx">
<p>copyright © 2023 KECO. All rights reserved.</p>
</div>
<div className="fot_btn fx">
<a
href="https://keco.or.kr/web/lay1/S1T6C1348/contents.do"
target="_blank"
>
개인정보처리방침
</a>
<a
href="https://keco.or.kr/web/lay1/S1T6C1349/contents.do"
target="_blank"
>
저작권정책안내
</a>
<a
href="https://keco.or.kr/web/lay1/S1T6C1350/contents.do"
target="_blank"
>
이메일무단수집거부
</a>
</div>
<p className="addr fx">
<span>
주소 : (22689) 인천광역시 서구 환경로 42(경서동
종합환경연구단지)
</span>
<span>
전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계) 032-590-3723,
3722
</span>
<span>팩스 :032-590-3729</span>
</p>
</div>
</div>
</form>
</div>
);
}
export { LoginPage };
@@ -0,0 +1,310 @@
/**
* OLYM Cascading Style Sheets
* File Name : login.css
* Description : 로그인 스타일시트
* Author : Web business Team / Choi Hyun Mi
* Email : desingtj@olym.co.kr
* Date : 2023.07.20
* Update : 2023.07.26
* Copyright(c) 2023 OLYM Communications. All Rights Reserved.
*/
html,
body {
background: #fff;
}
.fx {
display: flex;
-ms-display: flex;
-webkit-display: flex;
box-sizing: border-box;
}
.login {
width: 100%;
height: 100vh;
}
.login .left_box {
width: 100%;
background: url(/images/bg_login.jpg) no-repeat center/cover;
flex-direction: column;
justify-content: space-between;
padding: 7% 5% 5% 5%;
}
.login .left_box .text h1 {
font-size: 53px;
color: #fff;
}
.login .left_box .text h1 span {
font-weight: 800;
}
.login .left_box .text p {
margin-top: 40px;
font-size: 18px;
color: #fff;
}
.login .login_box {
width: 100%;
flex-direction: column;
justify-content: center;
padding: 4% 12% 4% 6%;
}
.login .login_box > ul {
flex-direction: column;
gap: 40px;
}
.login .login_box > ul > li h2 {
font-size: 22px;
font-weight: 600;
color: #2640a5;
margin-bottom: 14px;
}
.login .login_box > ul > li .input_st {
position: relative;
}
.login .login_box > ul > li .input_st .diveicon {
position: absolute;
top: 50%;
left: 28px;
transform: translateY(-50%);
font-family: "Xeicon";
font-size: 25px;
color: #003288;
}
.login .login_box > ul > li input {
width: 100%;
padding: 20px 20px 20px 70px;
background: #f7f7f7;
box-sizing: border-box;
border: none;
border-radius: 5px;
font-size: 18px;
font-weight: 700;
color: #000;
height: auto;
}
.login .login_box > ul > li input:focus {
outline: 1px solid #3c5fe5;
}
.login .login_box > ul > li input::placeholder {
color: #afabab;
font-weight: 500;
}
.login .login_box .bottom_box {
justify-content: space-between;
gap: 20px;
padding: 50px 0 45px 0;
}
.login .login_box .bottom_box .checks {
display: flex;
align-items: center;
gap: 8px;
}
.login .login_box .bottom_box .checks input[type="checkbox"] {
width: 18px;
height: 18px;
}
.login .login_box .bottom_box .checks label {
font-size: 14px;
}
.login .login_box .bottom_box .pw_message {
align-items: center;
gap: 5px;
color: #636363;
font-weight: 500;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
}
.login .login_box .bottom_box .pw_message i {
color: #327af5;
font-size: 20px;
}
.login .login_box .login_btn {
position: relative;
border-radius: 5px;
padding: 24px 15px;
background: linear-gradient(
to left,
#1d3969 0%,
#264078 10%,
#172558 40%,
#162254 50%,
#111c43 60%,
#111c42 100%
);
overflow: hidden;
justify-content: center;
align-items: center;
gap: 10px;
color: #fff;
cursor: pointer;
}
.login .login_box .login_btn span {
font-family: "Montserrat";
font-size: 19px;
font-weight: 700;
position: relative;
}
.login .login_box .login_btn i {
font-size: 22px;
transform: translateY(-2px);
}
.login .login_box .login_btn:before {
display: block;
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
background: linear-gradient(
to right,
#1d3969 0%,
#264078 10%,
#172558 40%,
#162254 50%,
#111c43 60%,
#111c42 100%
);
transition: opacity 0.5s;
}
.login .login_box .login_btn:hover::before {
opacity: 1;
}
.login .footer .copy {
margin-top: 36px;
justify-content: space-between;
align-items: center;
gap: 3%;
}
.login .footer .copy p {
font-size: 13px;
color: #d4d4d4;
width: 44%;
text-align: center;
}
.login .footer .copy::before,
.login .login_box .copy::after {
content: "";
width: 20%;
height: 1px;
background: #d4d4d4;
}
.login .footer .fot_btn {
gap: 20px;
margin: 30px 0 10px 0;
}
.login .footer .fot_btn a {
color: #333;
font-weight: 700;
font-size: 13px;
}
.login .footer .fot_btn a:hover {
color: #003288;
}
.login .footer .addr {
color: #666;
font-size: 13px;
gap: 3px 20px;
flex-wrap: wrap;
}
/* 비밀번호 변경 팝업창 */
.login .modal .modal_container {
width: 750px;
}
.login .pw_desc {
padding: 10px 10px 30px 10px;
color: #666;
}
.login .pw_desc p {
font-size: 1.25rem;
color: #000;
font-weight: 700;
margin-bottom: 10px;
}
.login .pw_desc span {
color: #f60;
font-weight: 700;
}
.login .modal .form_write {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
padding: 20px 50px;
}
@media (max-width: 1024px) {
.login {
flex-direction: column;
height: auto;
}
.login .left_box {
padding: 30px;
flex-direction: column-reverse;
gap: 20px;
}
.login .left_box .text h1 {
font-size: 30px;
line-height: 1.2em;
}
.login .left_box .text p {
margin-top: 20px;
font-size: 14px;
}
.login .left_box .img img {
height: 40px;
}
.login .login_box {
padding: 30px;
}
.login .login_box > ul {
gap: 20px;
}
.login .login_box > ul > li h2 {
margin-bottom: 7px;
}
.login .login_box > ul > li .input_st::after {
left: 15px;
font-size: 20px;
}
.login .login_box > ul > li input {
padding: 15px 10px 15px 50px;
font-size: 16px;
}
.login .login_box .bottom_box {
flex-direction: column;
align-items: flex-start;
padding: 30px 0;
}
.login .login_box .bottom_box .id_save label {
font-size: 14px;
}
.login .login_box .login_btn {
padding: 15px;
}
.login .login_box .login_btn span {
font-size: 16px;
}
.login .login_box .login_btn i {
font-size: 18px;
transform: translateY(-1px);
}
.login .login_box .copy p {
width: 100%;
}
.login .login_box .copy::before,
.login .login_box .copy::after {
display: none;
}
.login .footer .addr {
font-size: 11px;
gap: 3px 10px;
}
.login .modal .modal_container {
width: 90%;
}
.login .pw_desc {
padding: 0 0 20px 0;
}
.login .modal .form_write {
padding: 10px;
}
}
/* // login */
@@ -1,13 +1,20 @@
"use client";
import Image from "next/image";
import { useEffect } from "react";
import { images } from "@common/assets";
import { LoginApi } from "@/src/entities/auth";
import "./login.css";
function LoginPage() {
const hostname = window.location.hostname;
if (hostname.includes("hasuon.or.kr")) window.location.href = process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL||'/auth/login';
useEffect(() => {
const hostname = window.location.hostname;
if (hostname.includes("hasuon.or.kr")) {
window.location.href =
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
}
}, []);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();